2013年/10月/18日
首页回退
各种语言的的四则运算AST实现
写一个求值器,我们一般从四则运算表达式开始,我们从小到大写的代数表达式都是中缀表达式,它必须依赖括号来去除歧义,比如 12 * (24 - 5 ) / (17 + 6) ,其实还有另外两种表示法,
那就是前缀和后缀表达式,也叫波兰和逆波兰,他们不需要括号,而且非常便于计算机处理,其实这些表达式的本质是一个语法树,前缀和后缀是最靠近树型结构的,这也是lisp家族语言的括号表达式的本质,他们
本质就是对编程语言本质(抽象语法树)最自然的编码。
四则运算表达式的本质如图,是一个棵树
我们可以用字母代替部分数字,比如 a + b * 2,给这个表达式一个环境,比如a =1,b=2 就可以得到最终运算结果,我们省略对语法树的构建,而是直接用语言支持的结构写出来,比如在ruby中,我们可以通过数组[:add, [:variable, :a], [:multiply, [:number, 2], [:variable, :b]]]来表达a + b * 2,现在让我们来看看各种语言怎么执行这些表达式,这些程序都包括了结构,运算,递归,函数语言还有模式匹配。
ruby的第一版本,通过符号匹配,然后运算
上面第一个版本是通过case来寻找什么符号,然后再去找相应的运算,为何不去掉符号而直接把运算写在语法树中呢?那么有了ruby的第二版本,通过lambda表达式自执行
一个表达式的本质就是求值,它捕获环境,然后从环境中取值来计算,表达式应该是自封装的,比如a+b这个表达式应该封装运算和取值,至于是取a,取b还是取c由使用者决定,外部使用者通过指定取值key,比如a,b来创建一个封装体,封装体内部保存a和b,外部得到这个封装之后把它保存起来,这个封装体如果得到了环境就可以直接运行出结果,由于在编程语言中能够直接跑的就是函数,所以我们程序需要创建一个函数,同时这个函数有个环境保存取值key。由此,我们得到ruby实现的第三个版本,通过闭包,语法树不再是一个结构,而是一个闭包函数
在计算机科学中,我们把这种通过闭包变换来简化函数调用的方法叫做函数curry化。
用JavaScript翻译上面Ruby的实现,得到如下程序
Lisp方言racket的实现,它可以直接解析表达式,但是要写成前缀表达式,这是lisp方言的S表达式写法
对于上面的写法,引入大侠wangyin的言论很好解释(分割线内):
你必须从最简单的语言开始,逐步增加语言的复杂度,才能构造出正确的解释器。这篇文章就是告诉你如何写出一个最简单的语言 (lambda calculus) 的解释器,并且带有基本的的算术功能,可以作为一个高级计算器来使用。
一般的程序语言课程往往从语法分析(parsing)开始,折腾 lex 和 yacc 等麻烦却不中用的工具,解决一些根本不需要存在的问题。Parsing 的作用其实只是把字符串解码成程序的语法树(AST)结构。麻烦好久得到了 AST 之后,真正的困难才开始!而很多人在写完 parser 之后就已经倒下了。鉴于这个原因,这里我用所谓的“S-expression”来表示程序的语法树(AST)结构。S-expression (加上 Lisp 对它发自“本能”的处理能力)让我们可以直接跳过 parse 的步骤,进入关键的主题:语义(semantics)。
这里用的 Scheme 实现是 Racket。为了让程序简洁,我使用了 Racket 的模式匹配(pattern matching)。我对 Racket 没有特别的好感。但它安装比较方便,而且是免费的。如果你用其它的 Scheme 实现的话,恐怕要自己做一些调整。
Erlang VM 上的的语言Elixir,看看那个case
Scala的实现,也请注意那个case和match
参考资源