前言
提到学习编程,你是否立刻想到那些抽象难懂的代码和不知道有什么意义的术语,感到无从下手?
传统上,学习编程的过程被认为是一个积累工具的过程——先学变量赋值,学函数的写法,然后结合二者抽象成对象,逐步掌握该语言的特性后再尝试组合它们,看看能做出什么样的效果。下一步或许是学习更多的编程语言。这种方式让人产生一种误解,认为编程成功主要依赖反复尝试或需要特殊天赋。
今天,我想和大家分享一本可能会颠覆你的想法,让你对于编程有更清晰的认知的书——《How to Design Programs》。
它希望学习者对程序有根本的理解,通过系统性的方法思考,计划和设计程序。 作为比喻的话,它教你成为建筑师——先画设计图,再选工具施工。
本文是这本书第一章节部分内容的读后感。 我将用自己的话总结书中对“程序”的定义,然后解释被作者称为“设计配方”的程序设计方法。
这是一个非常基础但却常被忽视的话题。如果你对计算机程序没有太多了解,或者从未思考过计算机程序背后的逻辑,相信读完本文你会对这些概念有更深入的认知。
计算机程序
首先明确什么是计算机–计算机是一种能够接收输入、处理数据并输出结果的通用设备。
相比之下虽然计算器也能帮助我们进行计算,但它只能执行预设的功能, 而计算机则可以通过编程实现各种复杂任务,如图像处理、数据分析、游戏开发等。
从理论上看,计算机是一台通用计算设备——只要问题能被转化为明确的数学过程,它就能通过编程模拟解决方案。当需要计算机帮我们处理问题的时候,就需要告诉它具体的任务是什么,给它一些指令,这些指令就是计算机程序。 计算机程序的目的是描述计算机对用户信息的操作过程–它定义了如何接收输入、转换数据,并产生有意义的输出。
一个程序可以是一个可执行文件,如Windows的.exe或macOS的.app。 也可以是源代码的形式,如C语言的.c文件或Python的.py文件,这些源文件需要程序员编写。
要编写这些源代码,我们需要使用一种编程语言。就像跟不懂中文的老外交流,你得使用她听得懂得词汇组织成符合语法的句子才能交流。 (有趣的是,这些编程语言本身也需要一个程序来实现。本书的作者们是语义学领域的专家,他们常年在大学里面教解释器相关的课程, 也著有关于解释器的大作,我之后也会学习和介绍。)
程序员使用一种编程语言,编写一条条的指令(语句),这些代表计算机计算过程的指令的符号被称为表达式。 因此,编程就是编写表达式。而作者所倡导的系统性地编程,意味着要按照一定的逻辑和规则去组织表达式。
注意并不是所有代码中的语句都是表达式,至少不是在所有的编程语言中。拿最常见的两种编程语言JS和Python为例,在JS中这样的语句是可行的:let x; console.log(x = 5)
,但是在Python中运行print(x = 5)
会收到一个报错;同样If语句也不是表达式,因为他们不返回值。非要写成表达式的话,Python中也提供了赋值表达式和三元表达式,写成print(x := 5)
就没问题了。如果还没有学编程基础而看不懂上面的例子的话,也无需担心,只需要理解不同编程语言对表达式的处理方式存在差异,不过这个简化定义足以帮助我们理解核心概念。
函数
既然要系统性地组织表达式,那么就需要一种有效的组织方法。这本书中采用函数来组织表达式。
我们都在小学数学课上学过函数,虽然数学课上的函数和计算机程序中的函数在定义上存在一些差异, 但在本质上却是一致的,这有助于我们理解计算机程序中的函数。
在数学课上,我们学过的函数可能是这样的:y = x + 5 或者 f(x) = x + 5。当我们给 x 赋予不同的值时,f(x) 或者 y 的值也会相应变化。
在计算机程序中,函数被定义为一种组织表达式的方式,也就是一组指令。 它接收一个或多个参数作为输入,然后根据这些参数执行一系列操作,并最终返回一个值作为输出。
例如,在 Python 编程语言中,我们可以定义一个简单的函数来表达上面的例子:
def add5(x :int) -> int:
'''
Adds five to the provided integer.
Parameters: x (int).
Returns: int.
examples: add5(3) == 8;
add5(5) == 10.
'''
y = x + 5
return y
print(add5(3)) # Output: 8
print(add5(5)) # Output: 10
在这个例子中,我们定义了一个叫做add5的函数, 以后我们便可以使用这个函数而不需要写出具体的表达式来表达给某个数字加5。
与数学中的函数不同,计算机程序中的函数可以处理各种不同类型的数据,如数字、文本、列表等,而不仅仅是数字。 这意味着函数能够执行更为复杂和多样化的任务,例如对文本进行处理、对数据进行排序等。
程序与函数
我们之前说到程序接受信息产生信息,函数接收数据产生数据。这两者相互依赖: 程序是信息流动的完整管道(从输入到输出);函数是管道中的加工环节(对数据进行转换)。
数据和信息是都日常生活中常见的概念,然而很多人没有意识到的是: 程序所处理的信息和函数所处理的数据其实是同一事物的不同形态:在现实世界中我们叫做信息的东西,在计算机里面我们把它称为数据。
现实世界中的信息(如温度、文字)通过编码转化为计算机可处理的数据(如32位浮点数、UTF-8字节流), 而程序通过函数对这些数据进行操作,把处理过后的数据返回给用户,由用户解读成信息。
它们的不同点在于: 程序由真实世界的事件触发,比如用户的鼠标点击、键盘输入等;程序的输出内容旨在对真实世界产生影响,比如给用户提供一些信息、控制外部设备等。而函数在程序内部被定义和调用。
因为程序的核心就是数据和处理数据的方法(我们叫它函数)。 由此我们再更新一次对编程的定义:编程就是正确地定义数据,定义函数和调用函数的过程。
这里也需要说明,并不是所有人都会用这种把数据和函数分开的方式思考编程。很多编程语言也会故意模糊这两者的区别。软件工程师用 model-view-controller (MVC架构)来表示这种思想。普遍认为在设计复杂的程序的时候,应该分离二者。
如何编程
现在我们得到了好几个计算机程序的定义。“程序就是一堆表达式”,这已经是一个具有实用价值的定义,但是它意味着只要写出可以运行的表达式就是编程,这并不能很好地帮助我们使用程序这个工具来解决问题。
而数据与函数的思考方式帮助我们明确编程的主体和方向。我们需要思考在处理的问题中,我们想要的结果是什么,需要给出哪些信息,应该如何操作这些信息,以及如何在计算机中表示这些信息和操作过程。
这跟我们思考问题的过程非常一致,思考的时候,我们把现实世界中的信息转化为一条条的概念,然后又在头脑中组织操作这些概念。可以说编程的过程就是思考的过程。学习编程,就是学习思考。
优秀的程序员需要持续修炼两种基本功:
- 用合适的数据类型抽象现实信息;
- 用最少的计算资源完成数据转换。
我们知道,不同的信息应该使用不同的方式组织成数据,这些组织数据的方式被称为数据结构。(想更多了解数据结构请看我关于数据结构的博文。)对同一个操作信息的过程,也可能会用多种不同的方法表示,专业术语是算法。对于非专业人员,希望下次当听到有人说程序就是数据结构加算法的时候,你能有更多理解。
设计配方
现在我们了解了什么是程序,也有了关于如何使用它来解决问题的模糊印象,但是当你有一个具体的编程任务的时候可能还是会觉得很难写出代码。针对这个问题,作者提出了一系列设计程序的步骤,作为脚手架,帮助我们思考。他们称这些步骤为“设计配方”。
具体如下:
- 数据定义
- 明确需要表示哪些信息,并确定它们在编程语言中的表示方式
- 制定数据定义并通过示例说明
- 函数头部和签名
- 声明函数接收和产生的数据类型
- 简明回答”这个函数计算什么”的问题
- 定义符合签名的函数框架
- 功能示例
- 通过示例展示函数的目的
- 函数模板
- 将数据定义转化为函数的基本结构
- 函数实现
- 根据目的说明和示例填充函数模板
- 填补模板中的空缺部分
- 测试
- 将示例转化为测试用例,确保函数通过所有测试
- 测试可以发现错误,同时帮助他人理解函数定义
- 对于任何严肃的程序,测试都是必要的
其中每一个标题也是是每个步骤所得到的结果,每个子项目说明的目的和过程。
这个设计过程就是开头提到的先画图再施工的方法,作者们希望程序员严格地遵循这个流程,而不是尝试到程序能运行为止,并认为这是区分专业程序员和业余选手的一个重要标志。
我刚开始以为这个所谓的设计配方就是写一堆的注释,觉得很麻烦。就拿本文上面python代码为例,这么简单的代码,一眼就能看明白,完全没有必要写那么多注释。直到我看到了作者的演讲,才理解这个设计过程的意义。这个设计配方建立在以下几个想法之上:
-
不要急着写代码;一个经常发生的情况是代码写出来,发现和实际需求不匹配,重新修改代码的时间远远多于重新设计代码的时间。
-
用例子帮助自己理解问题;不管是在定于数据的阶段还是设计函数的阶段,例子都是核心内容,帮我们更好地理解问题,同时也能在函数实现后当作测试用例。
-
根据数据结构来设计函数的内部结构;乍一看这一步是最多余的,其实也是最重要的、最有启发性的。根据数据结构来组织代码,能帮你快速厘清层级关系,避免循环嵌套错误,使你的代码整洁逻辑清晰;经常思考数据结构和程序结构的关系帮助我们逐渐建立一种直觉——看到数据结构,就能预见代码结构;
-
一次性写出完美的代码是很难的,所以需要测试以及迭代。
上面这几个想法应该很容易理解。或许在处理逻辑简单直接的问题是,显得多余,但是对于更加复杂的问题,尤其是不知道该从哪里下手的时候不妨尝试作者们提出的设计流程。
结束
虽然这个设计流程在处理相对复杂的问题是才能体现出作用,但是如果不加以练习,到需要用的时候往往想不起来使用,因此这本书大部分内容就是在各种不同的场景下,面对不对的问题时如何调整设计方案来解决问题。
本书从原子数据开始逐渐涉及到一些更复杂的数据结构和设计模式。其中一些内容我会在后续的博文中介绍。感兴趣的话可以自己阅读原著。