(match exp [模式结果] [模式结果] ... ... ) 它根据表达式 exp 的“结构”来进行“分支”操作。每一个分支由两部分组成,左边的是一个“模式”,右边的是一个结果。左边的模式在匹配之后可能会绑定一些变量,它们可以在右边的表达式里面使用。
一般说来,数据的“定义”有多少种情况,用来处理它的“模式”就有多少情况。比如算术表达式有两种情况,数字或者 (op e1 e2)。所以用来处理它的 match 语句就有两种模式。“你所有的情况,我都能处理”,这就是“穷举法”。穷举的思想非常重要,你漏掉的任何一种情况,都非常有可能带来麻烦。所谓的“数学归纳法”,就是这种穷举法在自然数的递归定义上面的表现。因为你穷举了所有的自然数可能被构造的两种形式,所以你能确保定理对“任意自然数”成立。
那么模式是如何工作的呢?比如 '(,op ,e1 ,e2) 就是一个模式(pattern),它被用来匹配输入的 exp。模式匹配基本的原理就是匹配与它“结构相同”的数据。比如,如果 exp 是 '(+ 1 2),那
么 '(,op ,e1 ,e2) 就会把 op 绑定到 '+,把 e1绑定到 '1,把 e2 绑定到 '2。这是因为它们结构相同:
'(,op ,e1 ,e2) '( + 1 2) 说白了,模式就是一个可以含有“名字”(像 op, e1 和 e2)的“数据结构”,像 '(,op ,e1 ,e2)。我们拿这个带有名字的结构去“匹配”实际的数据(像 '(+ 1 2))。当它们一一对应之后,这些名字就自动被绑定到实际数据里相应位置的值。模式里面不但可以含有名字,也可以含有具体的数据。比如你可以构造一个模式 '(,op ,e1 42),用来匹配第二个操作数固定为 42 的那些表达式。
看见左边的模式,你就像直接“看见”了输入数据的形态,然后对里面的元素进行操作。它可以让我们一次性的“拆散”(destruct) 数据结构,把各个部件(域)的值绑定到多个变量,而不需要使用多个访问函数。所以模式匹配是非常直观的编程方式,值得每种语言借鉴。很多函数式语言里都有类似的功能,比如 ML 和 Haskell。
注意这里 e1 和 e2 里面的操作数还不是值,它们是表达式。我们递归的调用 interp1 自己,分别得到 e1 和 e2 的值 v1 和v2。它们应该是数字。 你注意到我们在什么地方使用了递归吗?如果你再看一下“算术表达式”的定义:
“算术表达式”有两种形式:
1. 一个数
2. 一个 '(op e1 e2) 这样的结构(其中 e1 和 e2 是两个“算术表达式”)
你就会发现这个定义里面“递归”的地方就是 e1 和 e2,所
以 calc 在 e1 和 e2 上面递归的调用自己。如果你在数据定义的每个递归处都进行递归,那么你的递归程序就会穷举所有的情况。
之后,我们根据操作符 op 的不同,对这两个值 v1 和 v2 分别进行操作。如果 op 是加号 '+,我们就调用 Scheme 的加法操作,作用于 v1 和 v2,并且返回运算所得的值。如果是减号,乘号,除号,我们也进行相应的操作,返回它们的值。
所以你就可以得到如下的测试结果:
(calc '(+ 1 2)) ;; => 3 (calc '(* 2 3)) ;; => 6 (calc '(* (+ 1 2) (+ 3 4))) ;; => 21 一个计算器就是这么简单。你可以试试这些例子,然后自己再做一些新的例子。
什么是 lambda calculus?
现在让我们过渡到一种更强大的语言:lambda calculus。它虽然名字看起来很吓人,但是其实非常简单。它的三个元素分别是是:变量,函数,调用。用传统的表达法,它们看起来就是:
? ? ?
变量:x 函数:λx.t 调用:t1 t2
每个程序语言里面都有这三个元素,只不过具体的语法不同,所以你其实每天都在使用 lambda calculus。用 Scheme 作为例子,这三个元素看起来就像:
变量:x
函数:(lambda (x) e) 调用:(e1 e2)
? ? ?
一般的程序语言还有很多其它的结构,可是这三个元素却是缺一不可的。所以构建解释器的最关键步骤就是把这三个东西搞清楚。构造任何一个语言的解释器一般都是从这三个元素开始,在确保它们完全正确之后才慢慢加入其它的元素。
有一个很简单的思维方式可以让你直接看到这三元素的本质。记得我说过,每个程序都是一个“机器的描述”吗?所以每个 lambda calculus 的表达式也是一个机器的描述。这种机器跟电子线路非常相似。lambda calculus 的程序和机器有这样的一一对应关系:一个变量就是一根导线。一个函数就是某种电子器件的“样板”,有它自己的输入和输出端子,自己的逻辑。一个调用都是在设计中插入一个电子器件的“实例”,把它的输入端子连接到某些已有的导线,这些导线被叫做“参数”。所以一个
lambda calculus 的解释器实际上就是一个电子线路的模拟器。所以如果你听说有些芯片公司开始用类似 Haskell 的语言(比如 Bluespec System Verilog)来设计硬件,也就不奇怪了。
需要注意的是,跟一般语言不同,lambda calculus 的函数只有一个参数。这其实不是一个严重的限制,因为 lambda calculus 的函数可以被作为值传递 (这叫 first-class function),所以你可以用嵌套的函数定义来表示两个以上参数的函数。比如,(lambda (x) (lambda (y) y)) 就可以表示一个两个参数的函数,它返回第二个参数。不过当它被调用的时候,你需要两层调用,就像这样:
(((lambda (x) (lambda (y) y)) 1) 2) ;; => 2 虽然看起来丑一点,但是它让我们的解释器达到终极的简单。简单对于设计程序语言的人是至关重要的。一开头就追求复杂的设计,往往导致一堆纠缠不清的问题。
lambda calculus 不同于普通语言的另外一个特点就是它没有数字等基本的数据类型,所以你不能直接用 lambda calculus 来计算像 (+ 1
2) 这样的表达式。但是有意思的是,数字却可以被 lambda calculus 的
三个基本元素“编码”(encoding) 出来。这种编码可以用来表示自然数,布尔类型,pair,list,以至于所有的数据结构。它还可以表示 if 条件语句等复杂的语法结构。常见的一种这样的编码叫做 Church encoding。