我是如何设计并实现一门程序设计语言——一门函数式编程语言Lucida的诞生


Lucida——一门函数式程序设计语言的诞生


起因

自己从本科那会就开始想写一个编程语言,不过那会缺乏编译的功底。自从阅读了DSL和Language Implementation Patterns,并写了一个一定规模的Parser之后,对编译有了一个比较全的认识,大概也能想清一个语言的运行机理和实现过程。而且到现在自己用过的语言也不少了,过程的有C,OO的有C++、C#、Java,脚本的有Javascript、Python,函数式的有Scheme、Haskell,看的书更是数都数不清,以至于到后来都觉的没什么意思了。

正好现在很多人都在热衷F#、Scala什么的函数式程序设计语言,说实话,与其去学这些东西,不如自己写一个出来,那这比学这些玩意要深刻的多。倒不是追求reinvent the wheels,我只是觉得现在的wheels未免太多了些,学完这个再学那个,这辈子也学不完。与其被这些东西搞的晕头转向,不如自己弄一个出来,以后再出个什么新东西也不至于会被弄的措手不及。

设计


经过短暂的思考,我决定以Scheme、Haskell和Python为基础,在尽可能短的时间内弄一门函数式编程语言出来,支持函数式编程的所有基本特性。此外,我需要为这个语言提供一个易用的interactive interface,就像GHCi或是IDLE那样。

为了便于构建解析器,我几乎毫不犹豫的选择了Scheme的RPN(逆波兰表达式)语法,尽管这个语法有些诡异:

    (def (f a) (* a a))
    (f 3) => 9

有了Scheme这个参考,构建语言的词法分析器和语法分析器就相当简单了,用了不到两个小时就搞出了Tokenizer和Parser,前端的部分就搞定了。

由于对代码生成不熟悉,况且我要弄的是一门解释型语言,因此有一个中间结构就足够了,这里我用了我比较熟悉的异构抽象语法树:通过一个基类型节点导出各种功能的子类型节点,同时子类型节点可以保存对基类型节点的引用,简单的来说就是Composite Pattern。

 

 

 

常用的语法结构无所谓就是定义,引用,调用函数,分支语句这些,只需按照之前写好的BNF逐一对应上就可以了,算上在草稿纸设计对象模型的时间,这部分大概用了我一个多小时。

接下来就是类型系统了,为了尽快的做出第一个原型,我挑选了四个最基本的类型:数字(Number),列表(List),函数(Function),名字(Name)。

 

数字自然保存一个整数值,直接使用CLR提供的Int64就足够了,这个很简单。

列表的设计花了我点时间,主要是在同构列表(Haskell风格)和异构列表(Python风格)之间抉择,但想到同构列表还要做类型推导这类蛋疼的工作,我选择了更简单的异构列表(而且功能似乎要更强大,只是无法提供静态类型检查)。

由于函数式语言中的函数也可以作为值传递,因此无需为它命名,只需设置它的形参和方法体即可。在这里它的方法体就是一段语法树,执行这个函数就是对语法树里进行实参代换,然后规约出结果即可,很自然的东西。

名称就是对上面这些类型实例的名字,名称保存一个对基类型的引用,因此它可以指向任何类型,这样使得函数赋值变的很简单,比如说:


 

这部分花的时间比较长,大概用了三个小时的样子,因为推翻了几个不靠谱的设计,多花了些时间。

接下来就是最外层的解释器了,在这里用的就是常用解释型语言的模式:Read and Eval:首先用Read读入代码,做一个简单的语法检查。然后再由Eval把这段代码执行(实际上就是转换成语法树并规约出结果),然后由控制台输出结果,这里不需要进行什么设计,倒是控制台的配色花了点时间,我向来很不爽GHCi或是IDLE的配色,因此我绝不会让我自己设计的解释器的配色方案跟它们一样烂:


 

做了几个简单的Test之后,把符号表弄了进去,测试了一下,变量赋值,函数递归的都能用,就是列表的递归有些问题,看了下时间,已经连续搞了快10个小时了,于是休息。第二天上午用了近两个小时才把bug揪出来,原来是函数生成List时共享了一部分内存引用,修改了就好使了。再次体会到除错的恶心。

接下来就是引入基本的操作了,这个很简单:写一个预定义的方法表,然后特定的操作符回调方法表中对应的方法。由于以前就用过这种手法,所以几乎没花什么时间,一个下午的时间,算术操作符,逻辑操作符以及基本的列表操作就搞定了,晚饭回来之后顺便写了三个最常用的高阶函数:map、reduce、filter。只要玩过函数式编程或是用过Python的人都知道这几个函数的作用,非一般的好用。



我一直很喜欢Haskell和Python的List Comprehension,于是就琢磨着把它弄进来,但考虑到RPN语法的限制,于是就参考了Python的range函数,以便快速生成一个list。



再次测试,由于都是很明显的东西,所以也出不了什么错。

事实上到这里Lucida已经成为了一个全功能的语言了:变量赋值,递归,列表,高阶函数,以及常用的内置函数,作为一个语言应该有的几样基本都具备了。

之前一直认为写个语言会是个很麻烦的事情,但写完了发现实际上也不算什么,anyway, who could not design and implement a programming language in two days?

接下来的工作


    引入浮点型,以及浮点型和整形数字的混合操作:

 

    引入匿名函数和Curry函数:

 

    引入字符和字符串类型,并设计它们与列表和数字之间的转换操作:
 

    为操作符添加一些有趣的属性:

    设置操作符映射,从而可以写出极其简短的代码:

最后就是写一个语言的手册了,这个更好写,直接仿照K&R的The C Programming Language写就行了。有兴趣的同学可以到这个链接下载阅读。

后记

 

算上这篇回顾,Lucida从无到原型,再到现在的基本完备,也就是不到一周时间的而已,所以在这里建议那些动不动就要学习新语言的同学停下来,花时间自己写一门语言出来,这比学语言深刻的多,再说写一个小语言也用不了多少时间。

刚刚看到坛子里有一个关于编译的很不错的连载博客,博主好像是叫装配脑袋吧(好奇怪的名字),他把编译那些常用的东西已经讲的很清楚了,认真的看一遍的话写一个语言是不成问题的。只可惜我看到的大多回复都毫无营养,只是毫无意义的顶来顶去。

Anyway, that's all. 有兴趣的话可以交流下编程语言的知识:lunageek@gmail.com



posted @ 2011-07-10 22:48 _Luc_ 阅读(...) 评论(...) 编辑 收藏