什么是计算机程序以及如何写一个程序?
前言
提到学习编程,你是否立刻想到那些抽象难懂的代码和不知道有什么意义的术语,感到无从下手?
传统上,学习编程常被比作制作家具,我们从简单的工具开始,逐步掌握更多技巧,不断尝试新工具以创作更复杂的产品。 这种方法也容易让人误解:编程成功依赖持续尝试直到成功,或者需要思维清晰与表达精准的天赋。
今天,我想和大家分享一本颠覆传统编程教育理念的书——《How to Design Programs》。
它希望学习者对程序有根本的理解,通过系统性的方法思考,计划和设计程序。 作为比喻的话,它教你成为建筑师——先画设计图,再选工具施工。
本文是这本书前言、序言以及第一章节部分内容的读后感。 我将用自己的话总结书中对“程序”的定义,并深入剖析他们所提出的“design recipe”(设计配方)这一系统性的程序设计方法。
这是一个非常基础但却常被忽视的话题。如果你对计算机程序没有太多了解,或者从未思考过计算机程序背后的逻辑,相信读完本文你会对这些概念有更深入的认知。
计算机程序
要了解计算机程序,首先我们需要明确什么是计算机。计算机是一种能够接收输入、处理数据并输出结果的通用设备。
相比之下虽然计算器也能帮助我们进行计算,但它只能执行预设的功能, 而计算机则可以通过编程实现各种复杂任务,如图像处理、数据分析、游戏开发等。
当我们要让计算机按照我们想要的方式运行时,就需要给它一些指令,这些指令就是计算机程序。 计算机程序的目的是描述计算机对用户信息的操作过程。一个接受用户信息,产生一些信息的过程。
程序可以是我们很熟悉的可以直接运行的形式,也可以以源代码的形式存在,我们可以在 GitHub 等平台上看到大量的源代码。
要编写这些源代码,我们需要使用一种编程语言。就像跟不懂中文的老外交流,你得用她听得懂得词汇并符合一定得语法才能交流。 (有趣的是,这些编程语言本身也需要一个程序来实现。本书的作者是语义学领域的专家,他们对编程语言的研究非常深入, 有关于编译器的非常好的书,我后面会写一篇博客介绍。)
程序员使用一种编程语言编写一条条的指令,这些代表计算机指令的符号被称为表达式。 因此,编程就是编写表达式。而作者所倡导的系统性地编程,意味着要按照一定的逻辑和规则去组织表达式。
函数
既然要系统性地组织表达式,那么就需要一种有效的组织方法。这本书中采用函数来组织表达式。
我们都在小学数学课上学过函数,虽然数学课上的函数和计算机程序中的函数在定义上存在一些差异, 但在本质上却是一致的,这有助于我们理解计算机程序中的函数。
在数学课上,我们学过的函数可能是这样的: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
在这个例子中,我们定义了一个叫做add5的函数,这意味着我们的语言中多了“add5”这个词汇, 以后我们便可以使用这个词汇来对所有的int这个类型的数字进行处理得到一个比它大五的数字。
程序和函数的关系
我们之前说到程序接受信息产生信息,函数接收数据产生数据。它们的行为似乎非常相似,是的,程序和函数之间的关系非常密切。
一个可能会让你吃惊的事实是: 程序所处理的信息和函数所处理的数据是一回事儿——在现实世界中我们叫做信息的东西,在计算机里面我们把它成为数据。
现实世界中的信息(如温度、文字)通过编码转化为计算机可处理的数据(如32位浮点数、UTF-8字节流), 而程序通过函数对这些数据进行操作,把处理过后的数据返回给用户,由用户解读成信息。 程序有两种类型,批处理程序和交互式程序。交互式程序会持续追踪事件(比如鼠标点击)并生成反馈。
它们的不同点在于: 程序由真实世界的事件触发,比如用户的鼠标点击、键盘输入等, 而程序的输出内容旨在对真实世界产生影响,比如给用户提供一些信息、控制外部设备等。而函数在程序内部被定义和调用。
程序的核心就是数据和处理数据的方法(我们叫它函数)。 由此我们再更新一次对编程的定义:编程就是正确地定义数据,定义函数和调用函数的过程。
需要说明的是并不是所有人都认同这种把数据和函数分开思考的方式。很多编程语言也会故意模糊这两者的区别。 软件工程师用 model-view-controller (MVC架构)来表示这种思想。普遍的看法是精心设计的软件最好分离二者。
顺带一提,你可能也听说过程序=数据结构+算法这个说法,跟我们说的程序等于数据和对数据的操作有异曲同工之妙。 考虑到数据结构就是数据在计算机中的组织数据的方式,算法就是操作数据的方法,也就不难理解了。如果还没理解,请关注我后续的博客内容。
如何编程
因此从数据定义到程序的过程非常关键。我们需要明确程序处理什么样的问题,它接受哪些信息,生成哪些信息。 我们需要弄清楚我们选择编程语言里面已经包含了哪些数据结构和函数。需要我们自己创建哪些结构以及我们怎么处理用户数据(定义函数)。 最后一旦完成程序,我们需要检查程序是否按照预期运行。
举例来说,你在工作中需要从网页上抓取一些数据,保存到Excel表格中进行处理,你想编写一个Python程序帮你完成这个工作。
这个程序的输入是一个或者一列网站地址,写一个函数从网址得到数据。可能会用Request
等库来发起连接,
使用BeautifulSoup
库解析HTML数据,因为是表格结构可以选择Pandas库,将提取结果转换为pandas.DataFrame
表格结构。
(这个例子可能有点太复杂了,但是它是一个真实的需求)
这个过程是一个熟能生巧的过程。需要多思考多练习。
设计配方
以上这个过程可以很复杂,或许只需要多尝试也常常能写出有效的程序。 但是一个好的程序会明确这个程序解决什么问题,它的输入输出是什么,如何定义这些数据, 以及让它的维护者(通常是过了一段时间之后的自己)对它的工作方式有所预期。 这样,当需求发生变动的时候也只需要对相应的数据进行变动。
为了达到以上目标,作者总结出来的编程的六个步骤:
-
从问题分析到数据定义;
明确程序中需要处理的核心信息,用编程语言的数据类型建立对应关系,最后编写正式的数据定义,对于复杂的数据还需要附具体示例说明。 -
函数签名、用途说明与函数存根;
签名:声明函数的输入/输出数据类型。
用途:说明函数的功能目标。 函数存根(Stub):编写符合签名的空函数框架,用于占位和接口测试。 -
功能示例;
编写典型输入/输出案例,通过这些示例具体说明函数的预期行为。 -
函数模板;
根据数据定义构建函数结构框架,确定条件判断分支等基本结构。 -
函数实现;
在模板中填充具体逻辑代码,结合用途说明和示例来确保正确性。 -
测试;
将示例转化为自动化测试用例,确保函数通过所有测试。测试既用于发现错误,也作为可执行文档。
完成了第六步,如果测试不通过,或者功能改动还是需要回到第四步或者第二部。
作者称以上这个过程即适合开发短的只包含一个函数的程序,适合大型甚至超大型的项目。 并且这个过程甚至可以迁移到其他的技能上面,作为一个通用的解决问题的流程:
“步骤一:解析问题陈述,描述的实际问题;
步骤二:抽象提炼核心要素, 抽离具体情境,建立抽象模型;
步骤三:通过示例具象化核心要素, 用典型场景案例说明抽象模型;
步骤四:基于分析制定方案框架, 创建实施计划与结构化提纲;
步骤五:对照预期目标验证成果, 通过预设标准评估输出结果;
步骤六:基于缺陷实施迭代优化, 根据测试失败案例修正解决方案。”
既然是编程的步骤,当然我们要试一下运用它写一个程序。而且不使用书中使用的编程语言BSL而是用python作为示例:
pass
结束语
之前讲了函数是组织表达式的方式之一,我才你可能会问还有没有其他的组织表达式的方法,你可能也对一些面向对象的程序语言非常熟悉,你问为什么不提对象。
作者们只提到了函数是我们小学数学课上都学过的东西,比较容易理解。他们没说的是,这本书来自Racket编程语言社区, Racket原本是Scheme的实现方式之一,后来随着它的发展逐渐和Scheme的理念不兼容,因此分道扬镳。 而Scheme常被认为是Lisp的方言。
Lisp是一种非常古老却影响深远的编程语言。 在黑客与画家这本书中,作者称它是面向未来的编程的语言,因为本来是数学理论的实现,在过去的机器上遇到性能瓶颈, 随着计算机硬件按照摩尔定律进行发展,程序员更多地专注抽象思考,它的价值会越来越大。 我们可以相信这种思想是有深厚的理论基础的。
而且这种以函数为模块来组织信息的方式跟我们思考方式也很像–我们的思考过程也是一个处理信息的过程。使用这种方式也能帮助我们清晰思考。