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. 小結
這真的只是試水的東西,沒怎么好好設計,代碼也寫的很亂。不過做一輪下來對一些以前在書里看到的知識有了直觀的了解,也算是有收獲吧。最后附上運行圖一張: