原文地址:https://lisperator.net/pltut/
循环爆栈的问题
λanguage 没有实现循环。其实可以通过递归实现循环
下面就是等价于循环 1~10 的代码
print_range = λ(a, b) if a <= b {
print(a);
if a + 1 <= b {
print(", ");
print_range(a + 1, b);
} else println("");
};
print_range(1, 10);
如果将 range 从 10 增加到 1000,就会发现一个问题:“Maximum call stack size exceeded”。递归最终会消耗光 JavaScript 的堆栈。
当然可以不用递归,再多定义一个 for while 这样的迭代关键字,但首先想想如何在保留递归的同时解决此问题
我们将在下一章节解决此问题
缺乏数据结构的问题
我们的 λanguage 语言似乎只有三种类型:数字,字符串和布尔型。看起来不可能创建复杂的结构,比如对象或列表。但实际我们还有一种类型:函数。
事实证明,在 λ 演算(λ-calculus)中,我们能利用函数来构造任意的数据结构,甚至包括带有继承的对象。
下面我们将通过已有的 λanguage 特性实现数据结构
通过修改 λanguage 本身实现对象的部分留到后面的章节
列表的构造
理论
pair
我们期望有一个函数 cons,会构建一个持有两个值的特殊对象;这个特殊对象我们称为 cons cell 或 pair
。
两个值一个叫做 car,另外一个叫做 cdr。对于一个给定的 cell 对象,我们可以使用函数 car 和 cdr 来获取对应的值。所以:
x = cons(10, 20);
print(car(x)); # prints 10
print(cdr(x)); # prints 20
从 pair 到列表
一个列表就是一个 cell 对象:
- 该对象在其 car 属性中保存了首个(first)元素
- 在 cdr 属性中保存了剩下(rest)的元素,而 cdr 这个元素又是一个 cell 对象,如此递归下去
Q:何时递归停止?
A:cdr 为空列表时停止
我们引入一个新的特殊对象 NIL
,代表空列表
可以像 cell 一样来操作 NIL(可以应用 car 和 cdr,但是结果是其自身)— 但它不是一个真正的 cell
定义列表就可以像这样:
x = cons(1, cons(2, cons(3, cons(4, cons(5, NIL)))));
查找列表中的第 i 项就可以这样
print(car(x)); # 1
print(car(cdr(x))); # 2 Lisp 中可以简写为 cadr(x)
print(car(cdr(cdr(x)))); # 3 caddr(x)
print(car(cdr(cdr(cdr(x))))); # 4 cadddr(x)
print(car(cdr(cdr(cdr(cdr(x)))))); # 5 到了第五项就没有简写了
基础 cons car cdr NIL 的实现
cons = λ(a, b) λ(f) f(a, b); # 接收两个值 (a, b)
# 返回一个函数,这个函数 cell 对象
# 等价于:
# cons = function(a, b){
# return function(f) {
# f(a, b);
# }
# };
car = λ(cell) cell(λ(a, b) a); # 接收一个 cell 函数对象
# 调用 cell,传入的参数是一个函数,该函数会接受两个参数并返回第一个参数。
# 等价于:
# car = function(cell){
# return cell(function(a, b){return a;});
# }
cdr = λ(cell) cell(λ(a, b) b); # 类似 car, 但是传入的函数会返回第二个参数。
NIL = λ(f) f(NIL, NIL); # 模仿一个 cell
# 等价于:
# NIL = function(f){
# return f(NIL, NIL);
# }
# 用两个 NIL 来作为函数参数,所以 car(NIL) 和 cdr(NIL) 都会返回 NIL
x = cons(1, cons(2, cons(3, cons(4, cons(5, NIL))))); # 生成一个 {1, 2, 3, 4, 5} 的列表
其他和列表有关的实现,多用到递归
foreach 的实现
foreach = λ(list, f)
if list != NIL {
f(car(list));
foreach(cdr(list), f);
};
# 等价于
# foreach = function(list, f){
# if (list != NIL) {
# f(car(list));
# foreach(cdr(list), f);
# };
# }
# 打印列表中的每一个元素,一行一个
foreach(x, println);
range:通过首尾元素构建列表
range = λ(a, b)
if a <= b then cons(a, range(a + 1, b))
else NIL;
# range = function(a, b){
# if (a <= b){
# return cons(a, range(a + 1, b));
# }
# else {
# return NIL;
# }
# }
# 打印1~8的平方
foreach(range(1, 8), λ(x) println(x * x));
可变的 car 和 cdr
目前为止我们的列表都是不可变的
大多数 Lisp 方言都提供了改变 cons cell 对象的方法。
Scheme 中叫 set-car! / set-cdr!。Common Lisp 中富有提示性的命名为 rplaca / rplacd。这次我们更倾向于使用 Scheme 的命名:
cons = λ(x, y)
λ(a, i, v)
if a == "get"
then if i == 0 then x else y
else if i == 0 then x = v else y = v;
car = λ(cell) cell("get", 0);
cdr = λ(cell) cell("get", 1);
set-car! = λ(cell, val) cell("set", 0, val);
set-cdr! = λ(cell, val) cell("set", 1, val);
# NIL 这时候可以是一个真正的 cons
NIL = cons(0, 0);
set-car!(NIL, NIL);
set-cdr!(NIL, NIL);
## 测试:
x = cons(1, 2);
println(car(x));
println(cdr(x));
set-car!(x, 10);
set-cdr!(x, 20);
println(car(x));
println(cdr(x));
其他数据结构的构造
通过列表的构造,展示了 λanguage 实现复杂可变数据结构的能力,实现对象的过程也是相似的
但是仅仅是使用 λanguage 实现,而不是直接更改 λanguage 使它支持对象,会使得对象非常笨重。
要是能在分词器 / 解析器中引入新的语法,并在求值器中增加新的语义就好了
很多时候为了取得可接受的性能,这也是所有主流语言的实现方式。下一节我们将来探索增加一些新的语法