let表达式
let表达式用来声明一个变量。 比如我们正在写一个模拟掷骰子游戏的程序。 一个骰子有6个面。 所以这个程序多次用到了6这个数字。 有一天,我们忽然改变主意,要玩12个面的骰子。 于是我们不得不仔细查找源代码,把里面的6改成12。 对于一个较大的程序,这是灾难的开始。 有时我们会漏掉几个6,有时我们会把几个指的不是骰子面数的6误改成12。 这种灾难被称作“魔术数字”。 避免魔术数字的方法一般是声明一个变量——比如说变量\(a\)——让这个变量等于6(\(a=6\))。 这个例子的let表达式包含三个元素:变量\(a\),要赋予变量的值6,以及程序主体\(M\)。 我将这条let表达式写成下面的样子: \[ ({let} \; a \; 6 \; M) \] 一般地,定义let表达式为如下形式: \[ ({let} \; X \; N \; M) \] 这是一个单变量的let表达式。
还是掷骰子的例子。 避免魔术数字还有一种方法是定义一个函数,函数的参数是骰子的面数\(a\),函数体是程序主体\(M\): \[ \lambda a.M \] 然后以参数6调用这个函数: \[ (\lambda a.M \; 6) \] 将上面这个表达式与let表达式对比,可以看到let表达式不过是函数调用的语法糖: \[ ({let} \; X \; N \; M) = (\lambda X.M \; N) \] 基于尽量简单的原则,我不打算将let表达式加入到解释器的语法中, 而是让let表达式以宏的形式加入语言。 所以另外写了一个函数translate来展开let表达式。
解释器先调用translate做转换,再调用value-of求值。
布尔类型
加入布尔类型可以像加入整数一样,定义布尔类型为基本类型,然后定义几个和布尔类型相关的表达式。 不过基于“以玩的心态写代码”的原则,我打算折腾一下,用编码的方式引入布尔类型。
可以说,布尔类型唯一的用途就是用于选择(二选一)。可以将真(true)理解为一个“两个中选择第一个”的函数,将假(false)理解为一个“两个中选择第二个”的函数。如下定义布尔类型: \begin{eqnarray*} {true} &=& \lambda x.\lambda y.x \\ {false} &=& \lambda x.\lambda y.y \end{eqnarray*}
为了将整数类型和布尔类型联系起来,需要添加一个判断一个整数是否为零的基本函数iszero。 \begin{eqnarray*} M, N, L &=& ... \\ &|& ({iszero} \; b) \end{eqnarray*} iszero的求值过程: \begin{eqnarray*} eval(({iszero} \; 0)) &=& {true} \\ eval(({iszero} \; b)) &=& {false}, 其中b \neq 0 \end{eqnarray*} 代码:
if表达式
if表达式定义为: \[ ({if} \; L \; M \; N) = ((L \; M) \; N) \]
上面if表达式的定义在call-by-value的调用方式会有问题。 在call-by-value的调用方式下,不论\(L\)的值是真是假,\(M\)和\(N\)都会被求值。 这不仅造成了多余的计算,在一些情况下会很悲剧: 如果\(M\)或者\(N\)有副作用(后面会加入一些有副作用的表达式),很可能会导致结果不正确; 如果这个if表达式在一个递归函数的函数体里,那么调用这个递归函数会无限循环。
为了避免\(M\)和\(N\)被提前求值,这里用一个技巧来延后\(M\)和\(N\)的求值。 将if表达式的定义改为: \begin{eqnarray*} ({if} \; L \; M \; N) &=& (((L \; \lambda X.M) \; \lambda X.N) \; 0) \\ &&其中X \notin FV(M) \cup FV(N) \end{eqnarray*} 将\(M\)和\(N\)封装成\(\lambda X.M\)和\(\lambda X.N\),避免了\(M\)和\(N\)被求值。 等到\(L\)的真假值被求出并选择了\(\lambda X.M\)或\(\lambda X.N\)中的一个后, 将其应用到参数0上(0是随便选的,反正\(X\)这个参数在函数体\(M\)和\(N\)里也用不着)。 这个技巧在很多call-by-value的语言中用来模拟惰性求值。
和let表达式一样,if表达式以宏的形式加入到语言中。 Call-by-name的解释器:
Call-by-value的解释器: