我是靠谱客的博主 娇气海燕,最近开发中收集的这篇文章主要介绍Eloquent JavaScript 笔记 十一:A Programming Language,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述


1. Egg 语言示例

do(define(x, 10),
   if(>(x, 5),
      print("large"),
      print("small")))
相当于js代码:

x = 10;
if(x>5)
  print("large");
else
  print("small");

1. Parser

读取一段代码,把它转化成一种数据结构,这个数据结构可以精准的反映代码的程序结构。

Egg中所有的东西都是Expression,可以分成四类:

1. variable

    任意字符序列,不包括空格,不能是保留字

2. number

    一串数字

3. string

    双引号包含的一段text,不支持转义字符

4. application

    function,if,while,>, +, -, *, / 等等

   如上面第一段代码,有多少左括号,就有多少个application,括号中是参数。do, define, if, >, print 都是application。

   操作符是application,书写顺序和js不同。 >(x,5) 相当于 x>5。后面的两个print表达式是if的参数,也是两个分支。

Parser 会把每一个expression转换成一个对象,所有的expression会组成一个对象树。 例如:

> (x,5)

会解析成:

{
  type: "apply",
  operator: {type: "word", name: ">"},
  args: [
    {type: "word", name: "x"},
    {type: "value", value: 5}
  ]
}

expression对象有三种type:

1. "value"

    字符串或数值, 有一个value属性,包含字符串和数字值。如上面的常量 5 。

2. "word" 

    变量,有一个name属性,保存标识符的名称。如上面的变量 x 和 > 。

3. "apply"

    application,有一个operator属性,保存操作的表达式,有一个args属性,保存参数数组。如上面的 > (x,5) 。注意, > 本身是 "word" 类型,加上后面的括号和参数会组合成 "apply" 类型。

1. 表达式树


这是第一段代码的表达式树。树中的每个圆点都是一个Expression,带有子节点的都是 "apply" 类型。

1. Parser 的实现代码

function skipSpace(string) {
    var first = string.search(/S/);
    if (first == -1)
        return "";
    return string.slice(first);
}

function parseExpression(program) {
    program = skipSpace(program);
    var match, expr;
    if (match = /^"([^"]*)"/.exec(program))
        expr = {type: "value", value: match[1]};
    else if (match = /^d+b/.exec(program))
        expr = {type: "value", value: Number(match[0])};
    else if (match = /^[^s(),"]+/.exec(program))
        expr = {type: "word", name: match[0]};
    else
        throw new SyntaxError("Unexpected syntax: " + program);

    return parseApply(expr, program.slice(match[0].length));
}

function parseApply(expr, program) {
    program = skipSpace(program);
    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);
        expr.args.push(arg.expr);
        program = skipSpace(arg.rest);
        if (program[0] == ",")
            program = skipSpace(program.slice(1));
        else if (program[0] != ")")
            throw new SyntaxError("Expected ',' or ')'");
    }

    return parseApply(expr, program.slice(1));
}

function parse(program) {
    var result = parseExpression(program);
    if (skipSpace(result.rest).length > 0)
        throw new SyntaxError("Unexpected text after program");
    return result.expr;
}

console.log(parse("+(a, 10)"));

这是完整的Parser代码,它的核心是parseExpression()。 这是一个递归函数,输入字符串,返回一个表达式对象和剩余的字符串。子表达式也用这个函数解析。上图中的每个黑色圆点都需要调用一次parseExpression() 来生成。

代码本身还是有一点难度的,我是看了两遍,又动手敲了一遍才彻底理解。不过还好,代码比较短,有二十分钟就够了。


1. evaluator

把代码parse成语法树之后,就可以执行了。

function evaluate(expr, env) {
    switch (expr.type) {
        case "value":
            return expr.value;
        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") {
                throw new TypeError("Applying a non-function.");
            }

            return op.apply(null, expr.args.map(function (arg) {
                return evaluate(arg, env);
            }));
    }
}
evaluate() 的第一个参数是expression,就是刚刚解析好的语法树,当然,也可以是一部分,只要是合法的expression就可以。第二个参数是env,env中包含了所有定义的变量(类似于js的全局变量),也包含了Egg语言预定义的运算符(例如:+, -, *, /等)和关键字(true, false等)。

evaluate()函数体中对三种expression类型分别进行了处理,"value" 和 "word" 类型处理很简单。"apply" 类型的处理相当令人费解,需要先看看specialForm 和env:

var specialForms = Object.create(null);

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["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);

    // Since undefined does not exist in Egg, we return false,
    // for lack of a meaningful result.
    return false;
};

specialForms["do"] = function(args, env) {
    var value = false;
    args.forEach(function(arg) {
        value = evaluate(arg, env);
    });
    return value;
};

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;
    return value;
};

var topEnv = Object.create(null);

topEnv["true"] = true;
topEnv["false"] = false;

["+", "-", "*", "/", "==", "<", ">"].forEach(function(op) {
    topEnv[op] = new Function("a, b", "return a " + op + " b;");
});

topEnv["print"] = function(value) {
    console.log(value);
    return value;
};

可见,在specialForms和topEnv中,apply类型相关的operator都是function。所以,evaluate()的第13、16行,都会返回function。

至此,parser和evaluator的代码就完整了,可以执行一段Egg代码了。

再提供一个run()函数,它可以让我们的代码显得更整齐:

function run() {
    var env = Object.create(topEnv);
    var program = Array.prototype.slice.call(arguments, 0).join("n");
    return evaluate(parse(program), env);
}

run("do(define(total, 0),",
    "   define(count, 1),",
    "   while(<(count, 11),",
    "         do(define(total, +(total, count)),",
    "            define(count, +(count, 1)))),",
    "   print(total))");
// → 55

1. Function

我们的Egg语言还不支持function,现在加上它。添加一个关键字:fun 。

specialForms["fun"] = function (args, env) {
    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");

        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)
            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);
    }
};

只剩下瞻仰的份儿了,打死我也想不出来这种实现方法。背下来,背下来。

1. Compilation (编译)

我们刚刚实现的parser和evaluator,实际上是个解释器。可以在paser和“执行” 中间加一层“编译”,就是把解析完成的语法树转换成机器码(机器语言)。这样的话,执行效率会大大提升。 当然,也可以把Egg编译成js代码,也会大大提升执行效率。 TypeScript的编译器(transpiler)就是干这个用的。

1. Exercise: Arrays

Egg的Array: array(1,2,5)

实现array和两个方法length(array) ,element(array, i)

分析:

array后面有括号,所以,它是apply类型。parser不需要修改。

对于apply类型,eveluator 都是按function处理。要么在specialForms中添加定义,要么在env中添加定义,其实都可以。我们倾向于把特殊的语法词汇(如:if,do,while等)放在specialForms中,那么,array就放在topEnv中。而且,topEnv中已经有了print函数,把length 和 element 放在topEnv中也就顺理成章了。

js本身有Array,只需要把function的arguments (第四章讲过这个arguments) 转换成js的Array就可以了。

注意,arguments本身不是Array,但在某种程度上,它和Array同型,可以用Array.prototype.slice.call(arguments,0) 把它转换成真正的Array。 这是个trick,js中有很多这样的小技巧,后面的章节还会遇到类似的。

topEnv["array"] = function () {
    return Array.prototype.slice.call(arguments, 0);
};

topEnv["length"] = function (array) {
    return array.length;
};

topEnv["element"] = function (array, i) {
    return array[i];
};
加上array之后,执行一下下面的Egg代码,看看输出什么:
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))))");

1. Exercise: Closure

run("do(define(f, fun(a, fun(b, +(a, b)))),",
    "   print(f(4)(5)))");
根据前面对fun的定义,这段代码转换成js代码是

var f = function(a) {
    return function(b) {
        return a + b;
    }
};
console.log(f(4)(5));
所谓Closure,就是内部函数能得到外层函数的变量,如这里的 a。这是如何做到的呢?看fun的实现代码,这部分:

        var localEnv = Object.create(env);
        for (var i=0; i< arguments.length; i++) {
            localEnv[argNames[i]] = arguments[i];
        }

        return evaluate(body, localEnv);

注意localEnv,它包含了外层传入的env,还有本身的arguments,无论有多少层函数内嵌,这些env和arguments都会传下去。

localEnv = Object.create(env) 把env作为localEnv的prototype,而变量是作为env对象的属性存储的,所以局部变量不会覆盖全局变量。有多层函数嵌套,会生成多层prototype,互相之间不会影响。

js的Closure也是这个原理吗?

1. Exercise: Comments

修改skipSpace() 方法,使Egg支持单行注释,注释以 # 开头。

原来的:

function skipSpace(string) {
  var first = string.search(/S/);
  if (first == -1) return "";
  return string.slice(first);
}

修改后的:

function skipSpace(string) {
  var skippable = string.match(/^(s|#.*)*/);
  return string.slice(skippable[0].length);
}

注意,正则表达式中的 . 是不匹配 n 的。

试一下:

console.log(parse("# hellonx"));
// → {type: "word", name: "x"}

console.log(parse("a # onen   # twon()"));
// → {type: "apply",
//    operator: {type: "word", name: "x"},
//    args: []}


1. Exercise: Fixing Scope

给变量赋值 set:

specialForms["set"] = function(args, env) {
  if (args.length != 2 || args[0].type != "word")
    throw new SyntaxError("Bad use of set");
  var varName = args[0].name;
  var value = evaluate(args[1], env);

  for (var scope = env; scope; scope = Object.getPrototypeOf(scope)) {
    if (Object.prototype.hasOwnProperty.call(scope, varName)) {
      scope[varName] = value;
      return value;
    }
  }
  throw new ReferenceError("Setting undefined variable " + varName);
};

使用方法:

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

这里再一次重申了变量scope的实现方法。


最后

以上就是娇气海燕为你收集整理的Eloquent JavaScript 笔记 十一:A Programming Language的全部内容,希望文章能够帮你解决Eloquent JavaScript 笔记 十一:A Programming Language所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(50)

评论列表共有 0 条评论

立即
投稿
返回
顶部