[编译器试水]我的语言-plang

1. 丘奇数

lambda演算是图灵等价的,用lambda可以模拟自然数,其中最常见的是邱奇数

    0 = λf.λx.x
1 = λf.λx.f x
2 = λf.λx.f (f x)
3 = λf.λx.f (f (f x))

简单点说,就是用函数f在x上作用了几次来表示该数字为几。λf.λx.f x作用了一次,所以该数为1;λf.λx.f (f x)作用了两次,所以该数为2,;以此类推。

在plang里,lambda的定义完全照搬上面的形式,只做两处修改:1. 参数可以有多个,包含在括号内,比如λf.λx.f x表示成\(f,x).f x 2. 作用次数明写在函数后,比如λf.λx.f (f x)表示成]\(f,x).f^2 x。\(f,x).x与\(f).\(x).x是完全等价的。

在丘奇数的基础上可以定义后继函数

SUCC = λn.λf.λx.f(n f x)

该lambda输入丘奇数n(也就是f在x上作用了n次),返回n+1(也就是再多作用一次)。在plang里表示成

var SUCC = \(n,f,x).f^(n+1) x

同样的,加法和乘法的定义是

PLUS = λm.λn.λf.λx.m f (n f x)
MULT = λm.λn.λf.m(n f)

在plang里表示成

var PLUS = \(m, n).\(f,x).f^(m+n) x
var MULT = \(m,n).\(f,x). f^(m*n) x
//或者var MULT = \(m,n).\(f,x). f^m (f^n x)

2. 谓词逻辑

lambda也可以模拟谓词逻辑,与自然数类似,该类逻辑基于丘奇布尔值之上。TRUE和FALSE的定义如下

    TRUE := λx y.x
FALSE := λx y.y

丘奇布尔值用左和右来表示真假,如果输入两个参数返回左边,则该值为真;返回右边则为假。在plang里定义如下

var TRUE = \(x,y).x
var FALSE = \(x,y).y

基于丘奇布尔我们可以定义谓词逻辑如下:

    AND := λp q.p q FALSE
OR := λp q.p TRUE q
NOT := λp.p FALSE TRUE
IFTHENELSE := λp x y.p x y

以AND为例:

AND TRUE FALSE = (λp q.p q FALSE) TRUE FALSE = TRUE FALSE FALSE = (λx y.x) FALSE FALSE = FALSE
AND TRUE TRUE = (λp q.p q FALSE) TRUE TRUE = TRUE TRUE FALSE = (λx y.x) TRUE FALSE = TRUE
AND FALSE FALSE = (λp q.p q FALSE) FALSE FALSE = FALSE FALSE FALSE = (λx y.y) FALSE FALSE = FALSE
AND FALSE TRUE = (λp q.p q FALSE) FALSE TRUE = FALSE TRUE FALSE = (λx y.y) TRUE FALSE = FALSE

非常明显,就是不断用实参代替形参产生lambda body的过程。在plang里定义如下

var TRUE = \(x,y).x;
var FALSE = \(x,y).y;
var AND = \(p,q).p q FALSE;
var OR = \(p,q).(p TRUE q);
var NOT = \(p).(p FALSE TRUE);
var IFTHENELSE = \(p,x,y).(p x y);

值得注意的是IFTHENELSE,如果p=TRUE则选择两个参数里的左边那个也就是x,如果p=FALSE则选择右边那个也就是y,正好是if的语义所在。

3. 递归和lazy eval

纯粹lambda演算的递归有些复杂,要用到不动点理论,所以我偷了点懒,在plang里除了lambda还有具名函数

defun foo(n, m)
{
return IFTHENELSE (ISZERO n) m ( foo (PREF n) (ADD m TWO) );
};

以上就是函数foo的定义,用来计算n的倍数。函数内部并非必须只有一条语句(这就是我加一个return关键字的原因),比如上述函数改写成这样也是可以的

defun foo(n, m)
{
var a = ISZERO n;
var b = PREF n;
var c = ADD m TWO;
return IFTHENELSE a m ( foo b c );
};
一个函数最终具有的值由return语句代表的值决定。如果没有return语句,则由最后一条语句决定。
眼尖的同学已经发现了这是个尾递归,可以优化成循环。plang里的计算都是lazy eval,比如foo函数,在n不为ZERO时返回的值不是最终值,而是带有 (foo b c)语句和当前环境拷贝的特殊值,等到有人要用该值(比如要
print到屏幕上,或者要被输入实参做计算时),用while循环不停的eval (foo b c)函数,以及该函数返回的结果,直到能得出实际值为止。这个过程不会让调用栈溢出,但是while循环过程中会不停的创建新环境(用来对形参做约束)。
lazy eval的另一个好处是可以短路求值,比如下面的语句
IFTHENELSE (AND TRUE FALSE) (IO print “hello") (IO print "world");
如果要把IFTHENELSE的三个参数全部求完值再返回,那么"hello" 和 "world"两个字符串势必会被全部打印出来,laze eval则只会打印正确的那个。

4. IO

本人最喜欢的语言是Haskell,Haskell作为纯函数式语言解决IO的办法是把它们包在IO Monad里,很纯很巧妙,但坦白讲难用的要死。plang里没有这么高级的货色(其实是本人水平还未够班),还是在编译器里内置了一些IO的操作。

比如在屏幕上打印的语句是

IO print "hello, world"

plang把IO函数作为一个特殊函数,也就是说,你要是自己定义了一个函数叫IO,那么编译器会抛异常。IO函数的第一个参数是io命令,目前只print和readline两个,不过以后相加的话也很容易的。一个小的echo程序如下

defun main()
{
var a = IO readline;
IO print a;
return a;
};

顺便说一句,main函数也是特殊函数,其他函数只有被调用到了才会去解释内部细节,main函数则会被自动调用到。

5. 环境

所谓的环境,就是符号和值之间的对应表,比如这段代码段

var TRUE = \(x,y).x;
var FALSE = \(x,y).y;
var AND = \(p,q).p q FALSE;
var OR = \(p,q).(p TRUE q);
var NOT = \(p).(p FALSE TRUE);
var a = AND TRUE FALSE;
var b = OR TRUE FALSE;
var c = AND (OR TRUE FALSE) FALSE;
var ZERO = \(f).\(x).f^0 x;
var ONE = \(f).\(x).f^1 x;
var N = \(f).\(x).f^n x;
var IFTHENELSE = \(p,x,y).(p x y);
defun foo(x, y)
{
var c = \(p,q).x;
return c;
};
defun main()
{
var ret = IFTHENELSE (AND TRUE FALSE) (IO print HELLO) (IO print WORLD);
var ret1 = IFTHENELSE FALSE (IO print hello1) (IFTHENELSE TRUE (IO print hello2) (IO print hello3));
IO print HELLO WORLD;
IO print TRUE;
IO print a;
var a = (foo FALSE TRUE);
var b = (a FALSE FALSE);
IO print b;
return (foo TRUE FALSE);
};

它所生产的环境如下

TRUE: ((p1,p2)->(p1))[1]
FALSE: ((p1,p2)->(p2))[1]
AND: ((p1,p2)->(p1,p2,b3))[1]
OR: ((p1,p2)->(p1,b2,p2))[1]
NOT: ((p1)->(p1,b2,b3))[1]
a: ((p1,p2)->(p2))[1]
b: ((p1,p2)->(p1))[1]
c: ((p1,p2)->(p2))[1]
ZERO: ((p1)->((p2)->(p1,p2)))[0]
ONE: ((p1)->((p2)->(p1,p2)))[1]
N: ((p1)->((p2)->(p1,p2)))[n]
IFTHENELSE: ((p1,p2,p3)->(p1,p2,p3))[1]
foo: __function__[1]
main: __function__[1]

foo和main是函数,main函数在全局环境生成后会自动被调用,foo则不会,只有main函数执行到(foo FALSE TRUE);语句时才调用。调用foo函数时会生成新的环境,在该环境里符号'x'绑定在FALSE上,符号'y'绑定在TRUE上。

返回的节点包含了自己所依赖的环境,在上述代码里,main函数里的符号a绑定在一个lambda \(p,q).x;上,而该lambda里的x符号绑定在FALSE上。每个节点都拷贝有一份自己的环境,这么做很浪费空间,但可以确保所有的自由变量都是有值的,比如下面的语句

defun foo(a)
{
var b = \(x,y).a;
return b;
};

var f1 = foo 10;
var f2 = foo 20;
IO print ( f1 1 2);
IO print ( f2 1 2);

打印的结果是

10

20

符号查找先从节点自带的环境开始,找不到则接着找全局环境,依然找不到就抛异常。

6. 实现

语法分析部分园子里的装配脑袋 有介绍,并且龙书上写的无比详细就差把伪代码翻译成真实代码了,再加上lex,yacc等东西成熟度非常高,也就没什么好说的了。目前plang的实现是解释执行的,难度比编译执行小很多。一个程序段分为很多的语句,解释器逐条解释,每解释完一条往环境里丢一个映射。遇到函数就先留一个占位的,等到真被调用到在逐句执行函数里的语句,直到碰到return或最后一个语句。当一个lambda或函数要求值时,先生成一个新的环境,然后绑定形参到实参上,在返回body的计算结果。以语句"IO print ( AND TRUE FALSE );"为例,计算的过程如下:

a. print 的参数是一个特殊节点,拥有( AND TRUE FALSE )语句,所以要循环对该节点求值

b. 查找符号“AND", 在全局环境里,是一个lambda \(p,q).p q FALSE;

c. 生成新环境e1, p约束到TRUE, q约束到FALSE,返回的节点是"TRUE FALSE FALSE"语句,该节点的环境是e1

d. 符号TRUE在全局环境中找到,是一个lambda \(x,y).x,新生成环境e2, x约束到FALSE,y约束到FALSE,返回节点FALSE,该节点的环境是e2。

e. 求值并未结束,继续查找符号"FALSE",在全局环境里找到是一个lambda,求值完成,while循环跳出

f. 打印结果。

7. 小结

这真的只是试水的东西,没怎么好好设计,代码也写的很乱。不过做一轮下来对一些以前在书里看到的知识有了直观的了解,也算是有收获吧。最后附上运行图一张:

[编译器试水]我的语言-plang_第1张图片

你可能感兴趣的:(lang)