diff --git a/12.md b/12.md index 05be066..bebb240 100644 --- a/12.md +++ b/12.md @@ -1,26 +1,22 @@ ## 十二、项目:编程语言 -> The evaluator, which determines the meaning of expressions in a programming language, is just another program. +> 确定编程语言中的表达式含义的求值程序只是另一个程序。 > -> Hal Abelson and Gerald Sussman, Structure and Interpretation of Computer Programs -> -> When a student asked the master about the nature of the cycle of Data and Control, Yuan-Ma replied ‘Think of a compiler, compiling itself.’ -> -> Master Yuan-Ma, The Book of Programming +> Hal Abelson 和 Gerald Sussman,《计算机程序的构造和解释》 构建你自己的编程语言不仅简单(只要你的要求不要太高就好),而且对人富有启发。 -希望通过本章的介绍,你能发现构建自己的编程语言其实并不是什么难事。我经常感到某些人的想法聪明无比,而且十分复杂,以至于我都不能完全理解。不过经过一段时间的阅读和思考,我就发现这些想法其实也并没有想象中那么复杂。 +希望通过本章的介绍,你能发现构建自己的编程语言其实并不是什么难事。我经常感到某些人的想法聪明无比,而且十分复杂,以至于我都不能完全理解。不过经过一段时间的阅读和实验,我就发现它们其实也并没有想象中那么复杂。 -我们将创造一门名为Egg的编程语言。这是一门小巧而简单的语言,但是足够强大到能描述你所能想到的任何计算。该语言也支持函数这类简单的抽象。 +我们将创造一门名为Egg的编程语言。这是一门小巧而简单的语言,但是足够强大到能描述你所能想到的任何计算。它允许基于函数的简单抽象。 ### 11.1 解析 -程序设计语言中最直观的部分就是语法(syntax)或符号。解析器是一种程序,负责读入文本片段(包含程序的文本),并产生一系列与程序结构对应的数据结构。若文本不是一个合法程序,解析器会输出对应的错误信息。 +程序设计语言中最直观的部分就是语法(syntax)或符号。解析器是一种程序,负责读入文本片段(包含程序的文本),并产生一系列与程序结构对应的数据结构。若文本不是一个合法程序,解析器应该指出错误。 -我们的语言语法简单,而且具有一致性。Egg中一切都是表达式。表达式可以是变量、数字,或应用(application)。不仅函数调用属于应用,而且if和while之类的语言构造也属于应用。 +我们的语言语法简单,而且具有一致性。Egg中一切都是表达式。表达式可以是绑定名称、数字,或应用(application)。不仅函数调用属于应用,而且if和while之类的语言构造也属于应用。 -为了确保解析器的简单性,Egg中的字符串不支持反斜杠转义符之类的元素。字符串只是简单的字符序列(不包括双引号),并使用双引号包围起来。数值是数字序列。变量名由任何非空格字符组成,而且语法中具有特殊含义的单词也不能用于变量名。 +为了确保解析器的简单性,Egg中的字符串不支持反斜杠转义符之类的元素。字符串只是简单的字符序列(不包括双引号),并使用双引号包围起来。数值是数字序列。绑定名由任何非空白字符组成,并且在语法中不具有特殊含义。 应用的书写方式与JavaScript中一样,也是在一个表达式后添加一对括号,括号中可以包含任意数量的参数,参数之间使用逗号分隔。 @@ -31,11 +27,11 @@ do(define(x, 10), print("small"))) ``` -Egg语言的一致性体现在:JavaScript中的所有运算符(比如>)在Egg中都是变量,但是可以像其他函数一样调用。由于语法中没有语句块的概念,因此我们需要使用do构造来表示多个表达式的序列。 +Egg语言的一致性体现在:JavaScript中的所有运算符(比如>)在Egg中都是绑定,但是可以像其他函数一样调用。由于语法中没有语句块的概念,因此我们需要使用do构造来表示多个表达式的序列。 解析器的数据结构用于描述由表达式对象组成的程序,每个对象都包含一个表示表达式类型的type属性,除此以外还有其他描述对象内容的属性。 -类型为“value”的表达式表示字符串和数字。值类型的value属性包含对应的字符串和数字值。类型为“word”的表达式用于标识符(名称)。这类对象以字符串形式将标识符名称保存在name属性中。最后,类型为“apply”的表达式表示应用。该类型的对象有一个operator属性,指向其操作的表达式,还有一个args属性,指向参数表达式数组。 +类型为“value”的表达式表示字符串和数字。值类型的value属性包含对应的字符串和数字值。类型为“word”的表达式用于标识符(名称)。这类对象以字符串形式将标识符名称保存在name属性中。最后,类型为“apply”的表达式表示应用。该类型的对象有一个operator属性,指向其操作的表达式,还有一个args属性,持有参数表达式的数组。 上面代码中>(x,5)这部分可以表达成如下形式: @@ -50,7 +46,7 @@ Egg语言的一致性体现在:JavaScript中的所有运算符(比如>) } ``` -我们将这样一个数据结构称为表达式树。如果你将对象想象成点,将对象之间的连接想象成点之间的线,这个数据结构将会变成树形。表达式中还会包含其他表达式,被包含的表达式接着又会包含更多表达式,这类似于树上的每个分支都可能有新的分支。 +我们将这样一个数据结构称为表达式树。如果你将对象想象成点,将对象之间的连接想象成点之间的线,这个数据结构将会变成树形。表达式中还会包含其他表达式,被包含的表达式接着又会包含更多表达式,这类似于树的分支重复分裂的方式。 ![](../Images/00344.jpeg) @@ -67,54 +63,57 @@ Egg语言的一致性体现在:JavaScript中的所有运算符(比如>) ```js function parseExpression(program) { program = skipSpace(program); - var match, expr; - if (match = /^"([^"]*)"/.exec(program)) + let match, expr; + if (match = /^"([^"]*)"/.exec(program)) { expr = {type: "value", value: match[1]}; - else if (match = /^\d+\b/.exec(program)) + } else if (match = /^\d+\b/.exec(program)) { expr = {type: "value", value: Number(match[0])}; - else if (match = /^[^\s(),"]+/.exec(program)) + } else if (match = /^[^\s(),"]+/.exec(program)) { expr = {type: "word", name: match[0]}; - else + } else { throw new SyntaxError("Unexpected syntax: " + program); + } return parseApply(expr, program.slice(match[0].length)); } function skipSpace(string) { - var first = string.search(/\S/); + let first = string.search(/\S/); if (first == -1) return ""; return string.slice(first); } ``` -因为Egg的元素之间可以添加任意数量空格,所以我们不得不循环删除程序开头的空格。这就是skipSpace函数的作用。 +由于Egg和JavaScript一样,允许其元素之间有任意数量的空白,所以我们必须在程序字符串的开始处重复删除空白。 这就是`skipSpace`函数能提供的帮助。 -跳过开头的所有空格后,parseExpression使用三个正则表达式来检测Egg支持的三种简单(原子)元素:字符串、数值和单词。解析器根据不同的匹配结果构造不同的数据类型。如果这三种形式都无法与输入匹配,那么输入就是一个非法表达式,解析器就会抛出异常。SyntaxError是一个标准错误对象类型,运行无效JavaScript程序时会触发该异常。 +跳过开头的所有空格后,parseExpression使用三个正则表达式来检测Egg支持的三种原子的元素:字符串、数值和单词。解析器根据不同的匹配结果构造不同的数据类型。如果这三种形式都无法与输入匹配,那么输入就是一个非法表达式,解析器就会抛出异常。我们使用`SyntaxError`而不是`Error`作为异常构造器,这是另一种标准错误类型,因为它更具体 - 它也是在尝试运行无效的 JavaScript 程序时,抛出的错误类型。 -接下来,我们从程序代码中删去可以匹配的部分字符串,将剩余的字符串和表达式对象一起传递给parseApply函数。该函数检查表达式是否是一个应用,如果是应用则解析带括号的参数列表。 +接下来,我们从程序字符串中删去匹配的部分,将剩余的字符串和表达式对象一起传递给parseApply函数。该函数检查表达式是否是一个应用,如果是应用则解析带括号的参数列表。 ```js function parseApply(expr, program) { program = skipSpace(program); - if (program[0] != "(") + if (program[0] != "(") { return {expr: expr, rest: program}; + } program = skipSpace(program.slice(1)); expr = {type: "apply", operator: expr, args: []}; while (program[0] != ")") { - var arg = parseExpression(program); + let arg = parseExpression(program); expr.args.push(arg.expr); program = skipSpace(arg.rest); - if (program[0] == ",") + if (program[0] == ",") { program = skipSpace(program.slice(1)); - else if (program[0] != ")") + } else if (program[0] != ")") { throw new SyntaxError("Expected ',' or ')'"); + } } return parseApply(expr, program.slice(1)); } ``` -如果程序中的下一个字符不是左圆括号,说明当前表达式不是一个应用,parseApply会直接返回该表达式。 +如果程序中的下一个字符不是左圆括号,说明当前表达式不是一个应用,parseApply会返回该表达式。 否则,该函数跳过左圆括号,为应用表达式创建语法树。接着递归调用parseExpression解析每个参数,直到遇到右圆括号为止。此处通过parseApply和parseExpression互相调用,实现函数间接递归调用。 @@ -124,10 +123,11 @@ function parseApply(expr, program) { ```js function parse(program) { - var result = parseExpression(program); - if (skipSpace(result.rest).length > 0) + let {expr, rest} = parseExpression(program); + if (skipSpace(result.rest).length > 0) { throw new SyntaxError("Unexpected text after program"); - return result.expr; + } + return expr; } console.log(parse("+(a, 10)")); @@ -139,78 +139,80 @@ console.log(parse("+(a, 10)")); 程序可以正常工作了!当表达式解析失败时,解析函数不会输出任何有用的信息,也不会存储出错的行号与列号,而这些信息都有助于之后的错误报告。但考虑到我们的目的,这门语言目前已经足够优秀了。 -### 11.2 代码执行器 +### 11.2 求值器 -在有了一个程序的语法树之后,我们该做什么呢?当然是执行程序了!而这就是代码执行器的功能。我们将语法树和包含环境信息的对象传递给代码执行器,执行器就会执行语法树中的表达式,然后返回整个过程的结果。 +在有了一个程序的语法树之后,我们该做什么呢?当然是执行程序了!而这就是求值器的功能。我们将语法树和作用域对象传递给求值器,执行器就会执行语法树中的表达式,然后返回整个过程的结果。 ```js -function evaluate(expr, env) { - switch(expr.type) { - case "value": - return expr.value; +const specialForms = Object.create(null); - case "word": - if (expr.name in env) - return env[expr.name]; - else - throw new ReferenceError("Undefined variable: " + - expr.name); - case "apply": - if (expr.operator.type == "word" && - expr.operator.name in specialForms) - return specialForms[expr.operator.name](expr.args, - env); - var op = evaluate(expr.operator, env); - if (typeof op != "function") +function evaluate(expr, scope) { + if (expr.type == "value") { + return expr.value; + } else if (expr.type == "word") { + if (expr.name in scope) { + return scope[expr.name]; + } else { + throw new ReferenceError( + `Undefined binding: ${expr.name}`); + } + } else if (expr.type == "apply") { + let {operator, args} = expr; + if (operator.type == "word" && + operator.name in specialForms) { + return specialForms[operator.name](expr.args, scope); + } else { + let op = evaluate(operator, scope); + if (typeof op == "function") { + return op(...args.map(arg => evaluate(arg, scope))); + } else { throw new TypeError("Applying a non-function."); - return op.apply(null, expr.args.map(function(arg) { - return evaluate(arg, env); - })); + } + } } } - -var specialForms = Object.create(null); ``` -代码执行器为每一种表达式类型都提供了相应的处理逻辑。文字量表达式直接产生自身的值(例如,表达式100的计算结果是数值100)。对于变量而言,我们必须检查程序中是否实际定义了该变量,如果已经定义,则获取变量的值。 +求值器为每一种表达式类型都提供了相应的处理逻辑。字面值表达式产生自身的值(例如,表达式100的计算结果是数值100)。对于绑定而言,我们必须检查程序中是否实际定义了该绑定,如果已经定义,则获取绑定的值。 -应用则更为复杂。若应用有特殊形式(比如if),我们不会执行任何表达式,只是将表达式参数和环境传递给处理这种形式的函数。如果是普通调用,我们执行运算符,验证其是否是函数,并计算参数的执行结果,将结果作为参数传递给函数。 +应用则更为复杂。若应用有特殊形式(比如if),我们不会执行任何表达式,而是将表达式参数和环境传递给处理这种形式的函数。如果是普通调用,我们执行运算符,验证其是否是函数,并计算参数的执行结果,将结果作为参数传递给函数。 -我们会使用一般的JavaScript函数来表示Egg的函数。在定义特殊格式fun时,我们再回过头来看这个问题。 +我们使用一般的JavaScript函数来表示Egg的函数。在定义特殊格式fun时,我们再回过头来看这个问题。 evaluate的递归结构类似于解析器的结构。两者都反映了语言自身的结构。我们也可以将解析器和执行器集成到一起,在解析的同时执行表达式,但将其分离为两个阶段使得程序更易于理解。 -这就是解释Egg所需的全部代码。这段代码非常简单,但如果不定义一些特殊的格式,或向环境中添加一些有用的值,你无法使用该语言完成任何工作。 +这就是解释Egg所需的全部代码。这段代码非常简单,但如果不定义一些特殊的格式,或向环境中添加一些有用的值,你无法使用该语言完成很多工作。 ### 11.3 特殊形式 -specialForms对象用于定义Egg中的特殊语法。该对象负责将单词和执行特殊形式的函数关联起来。目前该对象为空,现在让我们添加一点代码。 +specialForms对象用于定义Egg中的特殊语法。该对象将单词和求解这种形式的函数关联起来。目前该对象为空,现在让我们添加`if`。 ```js -specialForms["if"] = function(args, env) { - if (args.length != 3) - throw new SyntaxError("Bad number of args to if"); - - if (evaluate(args[0], env) !== false) - return evaluate(args[1], env); - else - return evaluate(args[2], env); +specialForms.if = (args, scope) => { + if (args.length != 3) { + throw new SyntaxError("Wrong number of args to if"); + } else if (evaluate(args[0], scope) !== false) { + return evaluate(args[1], scope); + } else { + return evaluate(args[2], scope); + } }; ``` Egg的if语句需要三个参数。Egg会执行第一个参数,若结果不是false,则执行第二个参数,否则执行第三个参数。相较于JavaScript中的if语句,Egg的if形式更类似于JavaScript中的?:运算符。这是一条表达式,而非语句,它会产生一个值,即第二个或第三个参数的结果。 -Egg和JavaScript在处理条件值时有些差异。Egg不会将0或空字符串作为假,只有当值确实为false时,测试结果才为假。 +Egg和JavaScript在处理条件值时也有些差异。Egg不会将0或空字符串作为假,只有当值确实为false时,测试结果才为假。 我们之所以需要将if表达为特殊形式,而非普通函数,是因为函数的所有参数需要在函数调用前计算完毕,而if则只应该根据第一个参数的值,确定执行第二个或第三个参数。While的形式也是类似的。 ```js -specialForms["while"] = function(args, env) { - if (args.length != 2) - throw new SyntaxError("Bad number of args to while"); - - while (evaluate(args[0], env) !== false) - evaluate(args[1], env); +specialForms.while = (args, scope) => { + if (args.length != 2) { + throw new SyntaxError("Wrong number of args to while"); + } + while (evaluate(args[0], scope) !== false) { + evaluate(args[1], scope); + } // Since undefined does not exist in Egg, we return false, // for lack of a meaningful result. @@ -221,85 +223,84 @@ specialForms["while"] = function(args, env) { 另一个基本的构件块是do,会自顶向下执行其所有参数。整个do表达式的值是最后一个参数的值。 ```js -specialForms["do"] = function(args, env) { - var value = false; - args.forEach(function(arg) { - value = evaluate(arg, env); - }); - return value; +specialForms.do = (args, scope) => { + let value = false; + for (let arg of args) { + value = evaluate(arg, scope); + } }; ``` -我们还需要创建名为define的形式,来创建变量对变量赋值。Define的第一个参数是一个单词,第二个参数是一个会产生值的表达式,并将第二个参数的计算结果赋值给第一个参数。由于define也是个表达式,因此必须返回一个值。我们则规定define应该将我们赋予变量的值返回(就像JavaScript中的=运算符一样)。 +我们还需要创建名为define的形式,来创建绑定对绑定赋值。Define的第一个参数是一个单词,第二个参数是一个会产生值的表达式,并将第二个参数的计算结果赋值给第一个参数。由于define也是个表达式,因此必须返回一个值。我们则规定define应该将我们赋予绑定的值返回(就像JavaScript中的=运算符一样)。 ```js -specialForms["define"] = function(args, env) { - if (args.length != 2 || args[0].type != "word") - throw new SyntaxError("Bad use of define"); - var value = evaluate(args[1], env); - env[args[0].name] = value; +specialForms.define = (args, scope) => { + if (args.length != 2 || args[0].type != "word") { + throw new SyntaxError("Incorrect use of define"); + } + let value = evaluate(args[1], scope); + scope[args[0].name] = value; return value; }; ``` ### 11.4 环境 -evaluate方法接受一个对象作为参数,使用对象的属性名称表示Egg中的变量名,使用属性的值表示Egg中绑定到变量名上的值。让我们定义一个环境对象来表示全局作用域。 +`evaluate`所接受的作用域是一个对象,它的名称对应绑定名称,它的值对应这些绑定所绑定的值。 我们定义一个对象来表示全局作用域。 -我们需要先定义布尔变量才能使用之前定义的if语句。由于只有两个布尔值,因此我们不需要为其定义特殊语法。我们简单地将true、false两个变量与其值绑定即可。 +我们需要先定义布尔绑定才能使用之前定义的if语句。由于只有两个布尔值,因此我们不需要为其定义特殊语法。我们简单地将true、false两个名称与其值绑定即可。 ```js -var topEnv = Object.create(null); +const topEnv = Object.create(null); -topEnv["true"] = true; -topEnv["false"] = false; +topScope.true = true; +topScope.false = false; ``` 我们现在可以执行一个简单的表达式来对布尔值求反。 ```js -var prog = parse("if(true, false, true)"); -console.log(evaluate(prog, topEnv)); +let prog = parse(`if(true, false, true)`); +console.log(evaluate(prog, topScope)); // → false ``` -为了提供基本的算术和比较运算符,我们也添加一些函数值到环境中。为了确保代码短小,我们在循环中使用new Function来合成一批运算符,而不是分别定义所有运算符。 +为了提供基本的算术和比较运算符,我们也添加一些函数值到作用域中。为了确保代码短小,我们在循环中使用new Function来合成一批运算符,而不是分别定义所有运算符。 ```js -["+", "-", "*", "/", "==", "<", ">"].forEach(function(op) { - topEnv[op] = new Function("a, b", "return a " + op + " b;"); -}); +for (let op of ["+", "-", "*", "/", "==", "<", ">"]) { + topScope[op] = Function("a, b", `return a ${op} b;`); +} ``` 输出也是一个实用的功能,因此我们将console.log包装在一个函数中,并称之为print。 ```js -topEnv["print"] = function(value) { +topScope.print = value => { console.log(value); return value; }; ``` -这样一来我们就有足够的基本工具来编写简单的程序了。下面的run函数提供了一个便利的方式来编写并运行程序。它创建一个新的环境对象,并解析执行我们赋予它的单个程序。 +这样一来我们就有足够的基本工具来编写简单的程序了。下面的函数提供了一个便利的方式来编写并运行程序。它创建一个新的环境对象,并解析执行我们赋予它的单个程序。 ```js -function run() { - var env = Object.create(topEnv); - var program = Array.prototype.slice - .call(arguments, 0).join("\n"); - return evaluate(parse(program), env); +function run(program) { + return evaluate(parse(program), Object.create(topScope)); } ``` -Array.prototype.slice.call的运用是一个小技巧,我们可以借此将诸如arguments之类类似于数组的对象转换成实际数组,然后使用join来进行拼接。该方法的参数是run函数的所有参数,我们将每个参数当作程序的每一行来执行。 +我们将使用对象原型链来表示嵌套的作用域,以便程序可以在不改变顶级作用域的情况下,向其局部作用域添加绑定。 ```js -run("do(define(total, 0),", - " define(count, 1),", - " while(<(count, 11),", - " do(define(total, +(total, count)),", - " define(count, +(count, 1)))),", - " print(total))"); +run(` +do(define(total, 0), + define(count, 1), + while(<(count, 11), + do(define(total, +(total, count)), + define(count, +(count, 1)))), + print(total)) +`); // → 55 ``` @@ -309,45 +310,49 @@ run("do(define(total, 0),", 每个功能强大的编程语言都应该具有函数这个特性。 -幸运的是我们可以很容易地添加一个fun语言构造,fun将最后一个参数当作函数体,将之前的所有名称当作函数参数。 +幸运的是我们可以很容易地添加一个fun语言构造,fun将最后一个参数当作函数体,将之前的所有名称用作函数参数。 ```js -specialForms["fun"] = function(args, env) { - if (!args.length) +specialForms.fun = (args, scope) => { + if (!args.length) { throw new SyntaxError("Functions need a body"); - function name(expr) { - if (expr.type != "word") - throw new SyntaxError("Arg names must be words"); + let body = args[args.length - 1]; + let params = args.slice(0, args.length - 1).map(expr => { + if (expr.type != "word") { + throw new SyntaxError("Parameter names must be words"); + } return expr.name; - } - var argNames = args.slice(0, args.length - 1).map(name); - var body = args[args.length - 1]; + }); return function() { - if (arguments.length != argNames.length) + if (arguments.length != argNames.length) { throw new TypeError("Wrong number of arguments"); - var localEnv = Object.create(env); - for (var i = 0; i < arguments.length; i++) - localEnv[argNames[i]] = arguments[i]; - return evaluate(body, localEnv); + } + let localScope = Object.create(scope); + for (let i = 0; i < arguments.length; i++) { + localScope[params[i]] = arguments[i]; + } + return evaluate(body, localScope); }; }; ``` -就像在JavaScript中一样,Egg中的函数有其自己的局部环境。我们使用Object.create来创建一个新的对象,使得该对象既可以访问外部环境(其原型),也可以包含新变量而不必修改外部作用域。 - -由fun形式创建的函数会创建自己的局部环境,并将参数变量添加到其中。接着使用该环境对象执行函数体,并返回结果。 +Egg 中的函数可以获得它们自己的局部作用域。 `fun`形式产生的函数创建这个局部作用域,并将参数绑定添加到它。 然后求解此范围内的函数体并返回结果。 ```js -run("do(define(plusOne, fun(a, +(a, 1))),", - " print(plusOne(10)))"); +run(` +do(define(plusOne, fun(a, +(a, 1))), + print(plusOne(10))) +`); // → 11 -run("do(define(pow, fun(base, exp,", - " if(==(exp, 0),", - " 1,", - " *(base, pow(base, -(exp, 1)))))),", - " print(pow(2, 10)))"); +run(` +do(define(pow, fun(base, exp, + if(==(exp, 0), + 1, + *(base, pow(base, -(exp, 1)))))), + print(pow(2, 10))) +`); // → 1024 ``` @@ -355,11 +360,11 @@ run("do(define(pow, fun(base, exp,", 我们构建的是一个解释器。解释器直接读取由解析器产生的语法树,并执行对应的程序。 -编译是在解析和运行程序之间添加的另一个步骤:在运行程序前尽可能将程序转换成一些高效执行的形式。例如,在设计良好的编程语言中,使用每个变量时变量引用的内存地址都是明确的,而不需要在程序运行时进行动态计算。这样可以省去每次访问变量时搜索变量的时间,只需要直接去预先定义好的内存位置获取变量即可。 +编译是在解析和运行程序之间添加的另一个步骤:在运行程序前尽可能将程序转换成一些高效执行的形式。例如,在设计良好的编程语言中,使用每个绑定时绑定引用的内存地址都是明确的,而不需要在程序运行时进行动态计算。这样可以省去每次访问绑定时搜索绑定的时间,只需要直接去预先定义好的内存位置获取绑定即可。 一般情况下,编译会将程序转换成机器码(计算机处理可以执行的原始格式)。但一些将程序转换成不同表现形式的过程也被认为是编译。 -我们可以为Egg编写一个可供选择的执行策略,首先使用new Function,调用JavaScript编译器编译代码,将Egg程序转换成JavaScript程序,接着执行编译结果。若能正确实现该功能,可以使得Egg运行的非常快,而且实现这种编译器确实非常简单。 +我们可以为Egg编写一个可供选择的执行策略,首先使用Function,调用JavaScript编译器编译代码,将Egg程序转换成JavaScript程序,接着执行编译结果。若能正确实现该功能,可以使得Egg运行的非常快,而且实现这种编译器确实非常简单。 如果读者对该话题感兴趣,愿意花费一些时间在这上面,建议你尝试实现一个编译器作为练习。 @@ -371,7 +376,7 @@ run("do(define(pow, fun(base, exp,", 当需要完成一些任务时,相比于自己完成所有工作,借助于别人提供的功能是一种更高效的方式。虽然在本章中我们编写的语言就像玩具一样,十分简单,而且无论在什么情况下这门语言都无法与JavaScript相提并论。但在某些应用场景中,编写一门微型语言可以帮助我们更好地完成工作。 -这些语言不太像传统的程序设计语言。若JavaScript没有正则表达式,你可以使用这种子语言来编写自己的解析器和执行器。 +这些语言不需要像传统的程序设计语言。例如,若JavaScript没有正则表达式,你可以为正则表达式编写自己的解析器和执行器。 或者想象一下你在构建一个巨大的机械恐龙,需要编程实现恐龙的行为。JavaScript可能不是实现该功能的最高效方式,你可以选择一种语言作为替代,如下所示: @@ -391,44 +396,48 @@ behavior attack launch arm-rockets ``` -这通常被称为领域特定语言(Domain-specific Language),一种为表达极为有限的知识领域而量身定制的语言。它可以准确表达其领域中需需要表达的事物,而没有多余元素。这种语言比通用语言更具表现力。 +这通常被称为领域特定语言(Domain-specific Language),一种为表达极为有限的知识领域而量身定制的语言。它可以准确描述其领域中需要表达的事物,而没有多余元素。这种语言比通用语言更具表现力。 ### 11.8 习题 #### 11.8.1 数组 -在Egg中支持数组需要将以下三个函数添加到顶级作用域:array(……)用于构造一个包含参数值的数组,length(array)用于获取数组长度,element(array,n)用于获取数组中的第n个元素。 +在Egg中支持数组需要将以下三个函数添加到顶级作用域:array(...values)用于构造一个包含参数值的数组,length(array)用于获取数组长度,element(array,n)用于获取数组中的第n个元素。 ```js // Modify these definitions... -topEnv["array"] = "..."; +topEnv.array = "..."; -topEnv["length"] = "..."; +topEnv.length = "..."; -topEnv["element"] = "..."; +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))))"); +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 ``` #### 11.8.2 闭包 -我们定义fun的方式支持函数“封闭”其周围环境,就像JavaScript函数一样,函数体可以使用在定义该函数时可以访问的所有局部变量。 +我们定义fun的方式支持函数引用其周围环境,就像JavaScript函数一样,函数体可以使用在定义该函数时可以访问的所有局部绑定。 -下面的程序展示了该特性:函数f返回一个函数,该函数将其参数和f的参数相加,这意味着为了使用变量a,该函数需要能够访问f中的局部作用域。 +下面的程序展示了该特性:函数f返回一个函数,该函数将其参数和f的参数相加,这意味着为了使用绑定a,该函数需要能够访问f中的局部作用域。 ```js -run("do(define(f, fun(a, fun(b, +(a, b)))),", - " print(f(4)(5)))"); +run(` +do(define(f, fun(a, fun(b, +(a, b)))), + print(f(4)(5))) +`); // → 9 ``` @@ -443,7 +452,7 @@ run("do(define(f, fun(a, fun(b, +(a, b)))),", ```js // This is the old skipSpace. Modify it... function skipSpace(string) { - var first = string.search(/\S/); + let first = string.search(/\S/); if (first == -1) return ""; return string.slice(first); } @@ -460,11 +469,11 @@ console.log(parse("a # one\n # two\n()")); #### 11.8.4 修复作用域 -目前变量赋值的唯一方法是define。该语言构造可以同时实现定义变量和将一个新的值赋予已存在的变量。 +目前绑定赋值的唯一方法是define。该语言构造可以同时实现定义绑定和将一个新的值赋予已存在的绑定。 -这种歧义性引发了一个问题。当你尝试为一个非局部变量赋予新值时,你最后会定义一个局部变量并替换掉原来的同名变量(一些语言的工作方式正和这种设计一样,但是我总是认为这是一种不恰当的作用域处理方式)。 +这种歧义性引发了一个问题。当你尝试为一个非局部绑定赋予新值时,你最后会定义一个局部绑定并替换掉原来的同名绑定。一些语言的工作方式正和这种设计一样,但是我总是认为这是一种笨拙的作用域处理方式。 -添加一个类似于define的特殊形式set,该语句会赋予一个变量新值,若变量不存在于内部作用域不存在,则更新其外部作用域相应变量的值。若变量没有定义,则抛出ReferenceError(另一个标准错误类型)。 +添加一个类似于define的特殊形式set,该语句会赋予一个绑定新值,若绑定不存在于内部作用域不存在,则更新其外部作用域相应绑定的值。若绑定没有定义,则抛出ReferenceError(另一个标准错误类型)。 我们目前采取的技术是使用简单的对象来表示作用域对象,处理目前的任务非常方便,此时我们需要更进一步。你可以使用Object.getPrototypeOf函数来获取对象原型。同时也要记住,我们的作用域对象并未继承Object.prototype,因此若想调用hasOwnProperty,需要使用下面这个略显复杂的表达式。 @@ -472,18 +481,18 @@ console.log(parse("a # one\n # two\n()")); Object.prototype.hasOwnProperty.call(scope, name); ``` -这行代码从Object原型中获取hasOwnProperty,并在作用域对象之上调用该方法。 - ```js -specialForms["set"] = function(args, env) { +specialForms.set = function(args, env) { // Your code here. }; -run("do(define(x, 4),", - " define(setx, fun(val, set(x, val))),", - " setx(50),", - " print(x))"); +run(` +do(define(x, 4), + define(setx, fun(val, set(x, val))), + setx(50), + print(x)) +`); // → 50 -run("set(quux, true)"); +run(`set(quux, true)`); // → Some kind of ReferenceError ```