❑(代码中)没有长跳跃。
❑通过过程抽象将大问题分解为(多个)较小的部分,从而降低控制流的复杂性。过程是功能片段,可以接受输入,但不一定产生与问题相关的输出。
❑过程可以通过全局变量的方式共享状态。
❑通过调用一个又一个的过程(对共享状态进行增、改等)来最终解决更大的问题。
在此编程风格中,较大的问题被拆解为子单元,也就是过程。每个子单元负责处理一件事。在这种风格中,过程之间常常共享数据,以此来实现最终目标。此外,状态的改变可能取决于变量的先前值。据说这些过程对数据有副作用。计算按照以下方式进行:一个过程处理(数据)池中的一些数据,并为下一个过程准备数据。过程不返回数据,相反,它们只操作共享的数据。
示例程序实现如下。声明一个共享数据池(第5~7行):第一个变量data保存输入文件的内容;第二个变量words保存从data中提取的单词;第三个变量word_freqs包含单词-词频对。三个变量都被初始化为空列表。此数据由一组过程共享(第12~75行),每个过程负责完成特定任务:
❑read_file(path_to_file)过程(第12~19行)接受文件路径,并将该文件的全部内容与全局变量data的当前值关联起来。
❑filter_chars_and_normalize()过程(第21~30行)用空格替换data中的所有非字母、非数字字符。更换就地完成。
❑scan()过程(第32~39行)使用内置函数split扫描data中的单词,并将它们添加到全局变量words中。
❑remove_stop_words()过程(第41~52行)首先从文件中加载停用词列表,并附加单字母单词(第44~46行);然后,遍历words列表并从中删除所有停用词。此过程通过如下方式实现:首先,记录words列表中停用词位置的索引;然后,调用内置函数pop从words列表中删除这些停用词。
❑frequencies()过程(第54~66行)遍历words列表,并创建一个单词、词频对的列表。
❑sort()过程(第68~73行)将变量word_freqs的内容按照词频降序排列。此过程通过调用内置函数sort来实现,而sort函数能够接受一个带有2个输入参数的匿名函数,在本例中,word_freqs列表中每对单词、词频的第二个元素(索引为1)正好作为其中一个输入参数。
主程序从第79行到最后。这段程序是这种食谱风格特征最明显的地方。较大的问题被整齐地分解为(多个)较小的子问题,每个子问题都由一个单独的命名过程(named procedure)处理。主程序的工作包括发出一系列命令来调用相关的每个子程序,这就像按照食谱一步步烹饪一样。从另一个角度看,每一个过程都会改变共享变量的状态
,就像我们按照食谱烹饪时改变配料的状态一样。
随着时间而改变状态(即状态可变)的后果是,调用过程可能不是幂等的。也就是说,调用同一个过程两次可能会导致完全不同的状态,以及完全不同的程序输出。例如,如果调用过程read_file(path_to_file)两次,由于第19行代码(变量)赋值的累积性质,最终data变量中出现重复的数据。幂等函数或幂等过程是指那些无论被调用一次还是多次,都将得到完全相同的可观察效果的函数或过程。缺乏幂等性(在某些时候)被许多人视为程序代码错误的根源。
这种编程风格非常适合实现外部数据随着时间的推移会发生变化,而程序的行为又依赖于这些外部数据的计算任务,例如人机交互场景,在不同的时间点,程序提示用户输入不同类型的信息,稍后用户本人又可能修改这些输入信息,而程序的输出结果依赖于用户输入的所有数据。对于人机交互场景,保存状态并且随着时间的流逝而改变状态是自然而然的。
后面的章节将提到的一个问题是共享状态的颗粒度。在示例程序中,变量是全局变量,它们被全部的过程所共享。长期以来,在除了短程序之外的所有其他程序中,使用全局变量都被认为是一个坏主意。本书中讨论的许多其他编程风格中,过程之间共享变量的作用域都小得多。
事实上,多年来,为了限制某些特别的编码习惯带来的副作用,人们进行了许多有意思的规范编程风格的相关工作。
在系统层面,食谱风格的架构在实践中被广泛使用。这种风格的主要特征是组件共享和更改外部数据状态,例如存储在数据库中的数据的状态。
20世纪60年代,更多更大型的程序被不断地开发出来,这也挑战了当时的编程技术。当时,人们面临的主要挑战之一是如何让编写者以外的其他人理解程序。虽然,编程语言变得越来越有特色,但它们并不一定会放弃旧的构件。同一种功能的程序可以通过许多不同的方式来实现。20世纪60年代后期,从“让程序更容易被人理解”的角度,一场关于编程语言的哪些特性“好”、哪些特性“坏”的辩论开始了。由Dijkstra领导的这场辩论提倡:限制使用一些被认为有害的语言特性,例如GOTO,并呼吁使用更高级别的迭代结构(例如while循环)、过程(或称为“子程序”)和适当的模块化代码。并非所有人都认同Dijkstra的观点,但他的观点的确占了上风。这催生了结构化编程——本章中展示的这种风格,它与第4章中的非结构化编程或单体编程相对立。
Dijkstra, E.(1970). Notes on Structured Programming . Available from http://www.cs.utexas.edu/users/EWD/ewd02xx/EWD249.PDF
概要:Dijkstra是结构化编程最直言不讳的倡导者之一。这些笔记列出了Dijkstra对一般编程的一些想法。这是一篇经典文章。
Wulf, W. and Shaw, M.(1973). Global variable considered harmful. SIGPLAN Notices 8(2): 28-34.
概要:关于构建程序的更多意见。正如标题所说,本文反对全局变量:不仅一般的结构化编程反对,结构良好的编程也反对。
❑ 幂等性 :如果函数或过程被多次调用和一次调用时有完全相同的可观察的结果,它就是幂等的。
❑ 可变变量 :变量被赋予的值随着时间的变化能够改变。
❑ 过程 :过程是程序的子程序。它可能有也可能没有输入参数;可能有也可能没有返回值。
❑ 副作用 :副作用是程序可观察的部分变化。副作用包括写入(外部)文件或屏幕、读取输入(参数等)、更改可观察变量的值、引发异常等。程序通过副作用与外界交互。
1. 用另一种语言实现示例程序,但风格不变。
2. 修改示例程序:去掉全局变量,但仍然以命令式风格为主,各过程基本保持不变。
3. 在示例程序中,哪些过程是幂等过程,哪些不是?
4. 尽可能少地修改示例程序,但要使所有过程都变成幂等过程。
5. 采用食谱风格编写一个不同的程序。它的功能与示例程序的完全相同,但采用不同的过程。
6. 使用食谱风格编写“导言”中提出的任务之一。