习题
数组
在 Egg 中支持数组需要将以下三个函数添加到顶级作用域:array(...values)
用于构造一个包含参数值的数组,length(array)
用于获取数组长度,element(array, n)
用于获取数组中的第n
个元素。
// Modify these definitions...
topEnv.array = "...";
topEnv.length = "...";
topEnv.element = "...";
run(`
do(define(sum, fun(array,
do(define(i, 0),
define(sum, 0),
while(<(i, length(array)),
do(define(sum, +(sum, element(array, i))),
define(i, +(i, 1)))),
sum))),
print(sum(array(1, 2, 3))))
`);
// → 6
闭包
我们定义fun
的方式允许函数引用其周围环境,就像 JavaScript 函数一样,函数体可以使用在定义该函数时可以访问的所有局部绑定。
下面的程序展示了该特性:函数f返回一个函数,该函数将其参数和f的参数相加,这意味着为了使用绑定a,该函数需要能够访问f中的局部作用域。
run(`
do(define(f, fun(a, fun(b, +(a, b)))),
print(f(4)(5)))
`);
// → 9
回顾一下fun形式的定义,解释一下该机制的工作原理。
注释
如果我们可以在 Egg 中编写注释就太好了。例如,无论何时,只要出现了井号(#
),我们都将该行剩余部分当成注释,并忽略之,就类似于 JavaScript 中的//
。
解析器并不需要为支持该特性进行大幅修改。我们只需要修改skipSpace
来像跳过空白符号一样跳过注释即可,此时调用skipSpace
时不仅会跳过空白符号,还会跳过注释。修改代码,实现这样的功能。
// This is the old skipSpace. Modify it...
function skipSpace(string) {
let first = string.search(/\S/);
if (first == -1) return "";
return string.slice(first);
}
console.log(parse("# hello\nx"));
// → {type: "word", name: "x"}
console.log(parse("a # one\n # two\n()"));
// → {type: "apply",
// operator: {type: "word", name: "a"},
// args: []}
修复作用域
目前绑定赋值的唯一方法是define
。该语言构造可以同时实现定义绑定和将一个新的值赋予已存在的绑定。
这种歧义性引发了一个问题。当你尝试为一个非局部绑定赋予新值时,你最后会定义一个局部绑定并替换掉原来的同名绑定。一些语言的工作方式正和这种设计一样,但是我总是认为这是一种笨拙的作用域处理方式。
添加一个类似于define
的特殊形式set
,该语句会赋予一个绑定新值,若绑定不存在于内部作用域,则更新其外部作用域相应绑定的值。若绑定没有定义,则抛出ReferenceError
(另一个标准错误类型)。
我们目前采取的技术是使用简单的对象来表示作用域对象,处理目前的任务非常方便,此时我们需要更进一步。你可以使用Object.getPrototypeOf
函数来获取对象原型。同时也要记住,我们的作用域对象并未继承Object.prototype
,因此若想调用hasOwnProperty
,需要使用下面这个略显复杂的表达式。
Object.prototype.hasOwnProperty.call(scope, name);
specialForms.set = function(args, env) {
// Your code here.
};
run(`
do(define(x, 4),
define(setx, fun(val, set(x, val))),
setx(50),
print(x))
`);
// → 50
run(`set(quux, true)`);
// → Some kind of ReferenceError