提问:铁路与编程有什么联系?

398 阅读10分钟

铁路与编程有什么联系呢?

乍一看二者似乎没任何关系。毕竟随着蒸汽机车的诞生,早在 19 世纪初就进入了现代铁路时代。自此直到 100 多年以后,计算机专家才开始使用机器语言编程。二者完全不是一个时代的产物。但实际上,从编程语言入门,到数据结构与算法,再到操作系统原理、编译原理等计算机系的专业课,总能看到铁路,或者说总能看到能用铁路来类比的概念。

今天我们就从简单到复杂,沿着程序员的成长路径,看看编程世界中的几条铁路吧。

初级铁路——帮助初学者理解程序流程和数据结构的铁路

火车总是沿着预定的轨道行驶,每条线路都有明确的起点和终点,行驶的过程中还可能遇到分支。这一点倒是和程序很像,每个程序都有明确的执行路径,从入口(如主函数)开始,经过各种分支、循环和函数调用,最后结束。因此,对于编程的初学者,铁路能直观地表示出程序的流程。

img01-branch-validate-update_db.png

例如,这段铁路就可视化了更新数据库(UpdateDb)的流程。绿色的轨道表示正常的流程,只有数据通过校验(Validate)并且数据库工作正常,新数据才能覆盖旧数据。否则”火车“就要驶入红色的轨道,意味着程序进入了异常的流程。

熟悉了程序的顺序执行、分支和循环这 3 种主要流程后,就可以学习数据结构与算法了。栈是一种比较基础的数据结构,它的结构和操作也可以通过铁路来可视化。

img02-stack.png

图中用两段在末端交汇的铁路表现出了栈这种数据结构的特点——先进后出。先驶入底端轨道的列车车厢(相当于数据)只能稍后驶离,因为没有穿墙术,没法穿越排在它前面的车厢。

使用铁路来类比栈这一想法据说是由 E.W. Dijkstra(艾兹赫尔·维伯·戴克斯特拉,1930年5月11日-2002年8月6日)提出的。这位来自荷兰的计算机科学家可不得了,他对计算机科学产生了深远的影响,计算机专业课中的最短路径算法、用于解决进程同步问题的信号量都出自他的手。你不想、不敢或不能在代码中写 goto 语句也是因为他,因为他的论文 Go To Statement Considered Harmful(《GOTO声明的危害》)批评了goto 语句的不良影响。

我感觉 E.W. Dijkstra 应该是个铁道迷,他不仅建议用铁路来类比栈的结构和操作,就连他提出的信号量 semaphore 这个词本身,以及针对信号量的两种操作的命名(操作系统原理课程中的 PV 原语)也是和铁路相关的术语。在本文的最后,我们将再次看到 E.W. Dijkstra 的另一项成就,依然与铁路有关。

中级铁路——用来描述编程语言语法的铁路图

要成为能够独当一面的程序员,只熟悉一门编程语言是远远不够的。在学习一门新语言时,要先掌握语法,而语法是个比较复杂的概念,无法用一两句话说清。

入门级的图书往往会用具体的句型和示例来介绍语法,这就如同通过学校的英文教材学英语,难以接触到语法的本质。好在程序员比较熟悉正则表达式,可以用它来描述语法。比如,SQL 中最简单的 SELECT 语句的语法可以这样描述,

SELECT\s.*FROM\s.*WHERE\s.*

那复杂一些的 SELECT 语句该如何用正则表达式描述呢?也许是这样,

(?i)SELECT\s+[\w\*,\s]*\s+FROM\s+\w+(?:\s+JOIN\s+\w+)?\s+(?:WHERE\s+.+?)?(?:\s+ORDER\s+BY\s+\w+)?(?:\s+LIMIT\s+\d+)?

已经有些不好解读了吧,那能够匹配 JSON 的正则表达式又该怎么写呢?

这个别说咱们写不对了,就连 JSON 之父 Douglas Crockford(道格拉斯·克罗克福德)也是多次修改了他自己编写的正则表达式(和对正则表达式的操作),才能正确判断字符串是否是安全的 JSON。最初的判断只有 2 行,

// https://web.archive.org/web/20060226161035/http://www.json.org/json.js
!(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
    text.replace(/"(\\.|[^"\\])*"/g, '')))

但经过不断完善,最终的判断膨胀到,

// https://web.archive.org/web/20101127064455/http://www.json.org/json.js
/^[\],:{}\s]*$/.
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))

正则表达式越写越复杂,这背后的原因在于其表达能力上的限制,正则表达式无法描述括号匹配、嵌套结构和递归模式等复杂的语言结构。那该如何描述编程语言的语法呢?

还是以 JSON 为例,在 json.org 的首页上,同时使用了 2 种方式来描述 JSON 的语法。

对于 JSON 中的数组,页面的右侧用如下一段代码(术语叫作上下文无关文法)来描述,

array
    '[' ws ']'
    '[' elements ']'

elements
    element
    element ',' elements

element
    ws value ws

ws
    ""
    '0020' ws
    '000A' ws
    '000D' ws
    '0009' ws

简单来说,这段代码表示,

  • array 可以是一个空数组 [] 或者包含元素的数组 [ elements ]
  • elements 可以是单个元素 element 或者多个元素组成的列表 element ',' elements
  • element 是由空白字符 ws 包围的一个值 value
  • ws 既可以是空格 '0020'、换行 '000A'、回车 '000D'、制表符 '0009' 的排列组合,也可以为空

最不好理解的地方是“elements 可以是多个元素组成的列表 element ',' elements”,因为这里用 elements 本身解释了 elements,出现了循环(递归)。

Douglas Crockford 显然也意识到了这种描述方法虽然专业但不好理解(所谓的要理解递归需要先理解递归)。为了推广 JSON,使其成为取代 XML 的标准,需要让大部分程序员能够快速理解数组等 JSON 结构的样子。于是 Douglas Crockford 又画出了 JSON 中每种结构的铁路图

img03-json-array.png

这是 JSON 中数组的铁路图。想象一列火车沿着最上方的轨道呼啸而过,这就得到了一个空数组 [](也可以夹杂一些空白字符,如 [ ]);火车也可以经过 value “车站”后就掉头往回开(沿着最下方的轨道),于是在经过了 , 之后就会再次回到 value “车站”,然后循环往复,不断经过 ,value 这两“站”,最终不再掉头,选择上方的铁路开向终点 ],这就形成了包含多个 value 的数组。

铁路图不但简洁直观还有较强的表达能力,真是我们学习新语言语法的好工具。

高级铁路——后缀表示法与调度场算法

如果你是资深的程序员,是不是总想自创一门编程语言呢?这就需要再学习一些编译原理的知识了。

编译过程可以很复杂,如将一门新语言转化为可执行的程序,也可以很简单,仅仅是改变输入字符串的格式。算式的后缀表示法(postfix notation)就属于后者,是编译原理的入门课程常用的示例。

后缀表示法,也称为逆波兰表示法(Reverse Polish Notation,RPN),也是一种算式的表示方式,它不同于我们日常书写算式的方法(中缀表示法,infix notation),加减乘除等运算符要跟在数字的后面,而不是位于数字的中间。例如,3 + 4 × 2 的后缀表示法是 3 4 2 × +。可这样书写有什么好处呢?

最大的好处是便于机械地计算,而这正是计算机擅长的。只要遇到一个运算符,就可以立刻将相应的运算作用于它前面的两个数字,然后用计算结果取代这两个数字,并继续看下一个运算符。这使得计算过程可以从左到右一次性完成,而无需考虑运算的优先级,甚至可以消除算式中表示优先级的括号。

还是以 3 + 4 × 2 为例,如果我们用计算器来计算,那么就必须先依次输入 4×2=,因为“先乘除后加减”。然后把中间结果 8 记下来,再依次输入 3+8=,才能得到最终结果。

但如果算式已改写为后缀表示法的 3 4 2 × +,那么我们就可以忽略运算的优先级,从左到右不假思索地输入数字和运算符了。macOS 自带的计算器程序就支持后缀表示法,可使用快捷键 ⌘+R 开启该功能。

img04-PRN-calc

既然后缀表示法便于计算机处理,那该如何将我们习惯的算式转写为后缀表示法呢?对于 (3 + 4) × 5 - 6,也许经过几次尝试,能够轻松得出后缀表示法是 3 4 + 5 × 6 -,那

1 + 2 × (3 - 4)^2 + 5^(3 + 4 + 5) ,即

img05-complex_expre.png

的后缀表示法又是什么呢?这恐怕就要花费一番工夫了吧。

又轮到铁道迷 E.W. Dijkstra 出手了,他发明了调度场算法(Shunting Yard Algorithm),用于将惯用的中缀表示法转换为后缀表示法。而该经典算法又又又一次和铁路相关。现实中的调度场(如下图所示)是铁路车站集中处理大量列车到达、解体、编组、出发等作业的区域。

img06-shunting-yard.jpg

下面是一段调度场算法的演示动画。我们可以看到,该算法是如何利用 3 段铁路,通过模拟列车解体并重新编组的过程,将来自 Input 段铁路上、采用中缀表示法的一列“火车”(就是刚刚提到的算式 1 + 2 × (3 - 4)^2 + 5^(3 + 4 + 5)),重新排列成下方 Output 段铁路上采用后缀表示法的另一列“火车”。火车头不断地在 Input、Stack 和 Output 这 3 段铁路间牵引车箱,并将“装载着小括号”的车厢拉走。

通过反复观看这段动画,应该不难发现火车头的运动规律:

  • 如果“车箱装载着数字”,就将其直接带到 Output 段铁路
  • 如果”装载着运算符“,则要与停在 Stack 段铁路最右侧车厢上的运算符比较,再根据这两个运算符优先级的高低,决定下一步是把当前“车厢”直接带去 Output 段铁路,还是也停留在 Stack 段铁路
  • ……

具体的规则可参考维基百科 wiki/调度场算法#详细的算法


据传说,铁轨之间的标准宽度可以追溯到古罗马的马车道宽度,而那时的马车道的宽度是根据两匹马屁股的宽度决定的。而程序员往往也会是"屁股决定大脑",或在开发过程中权衡利弊做出取舍,或学着源码分析写着 CRUD。这也算是铁路和编程之间一种微妙的联系了吧。

胡译胡说 软件工程师、技术图书译者。译有《图解云计算架构》《图解量子计算机》《图解TCP/IP(第6版)》《计算机是怎样跑起来的》《自制搜索引擎》等。