https://liujiacai.net/blog/2014/10/12/lambda-calculus-introduction/
Lambda calculus我們一般稱為λ演算,最早是由邱奇(Alonzo Church,圖靈的博導)在20世紀30年代引入,當時的背景是解決函數可計算的本質性問題,初期λ演算成功的解決了在可計算理論中的判定性問題,后來根據Church–Turing thesis,證明了λ演算與圖靈機是等價的。
演算的語法與求值
因為λ演算研究的是函數的本質性問題,所以形式極其簡單:
E = x? ? ? ? ? variables
| λx. Efunctioncreation(abstraction)
| E1 E2functionapplication
上面的E稱為λ-表達式(expressions)或λ-terms,它的值有三種形式:
1,變量(variables)。
2,函數聲明或抽象(function creation/abstraction)。需要注意是的,函數中有且僅有一個參數。在λx. E中,x是參數,E是函數體
3,函數應用(function application)。也就是我們理解的函數調用,但官方術語就叫函數應用,本文后面也會采用“應用”的叫法。
λ表達式例子
上面就是λ演算的語法了,很是簡單吧。下面看幾個例子:
恒等函數
λx.x
一個返回恒等函數的函數
λy. (λx.x)
可以看到,這里的y參數直接被忽略了
在使用λ演算時,有一些慣例需要說一下:
1,函數聲明時,函數體盡可能的向右擴展。什么意思呢,舉個例子大家就明白了
λx.xλy.xy z 應該理解為 λx. (x(λy. ((xy) z)))
2,函數應用時,遵循左結合。在舉個例子:
x y z 應該解釋為 (x y) z
Currying帶有多個參數的函數
從上面我們知道,λ演算中函數只有一個參數,那兩個參數的函數的是不是就沒法表示了呢,那λ演算的功能也太弱了吧,這就是λ的神奇之處,函數在本質上只需要一個參數即可。如果想要聲明多個參數的函數,通過currying技術即可。下面來說說currying。
λx y. (+ x y)---->λx. (λ y. + x y)
上面這個轉化就叫currying,它展示了,我們如何實現加法(這里假設+這個符號已經具有相加的功能,后面我們會講到如何用λ表達式來實現這個+的功能)。
其實就是我們現在意義上的閉包——你調用一個函數,這個函數返回另一個函數,返回的函數中存儲保留了調用函數的變量。currying是閉包的鼻祖。
如果用Python來表示就是這樣的東西:
def add(x):
????????return? ?lambday: x+y
add(4)(3) //return7
如果用函數式語言clojure來表示就是:
(def nadd [x]
????(fn [y] (+ x y)))
((add 4) 3); return 7
求值(evaluation)
在λ演算中,有兩條求值規則:
1,Alpha equivalence( or conversion )
2,Beta reduction
Alpha equivalence
這個比較簡單也好理解,就是說λx.x與λy.y是等價的,并不因為換了變量名而改變函數的意義。
簡單并不說這個規則不重要,在一些變量覆蓋的場合很重要,如下這個例子:
λx. x (λx. x)如果你這么寫的話,第二個函數定義中的x與第一個函數定義中的x重復了,也就是在第二個函數里把第一個的x給覆蓋了。
如果改為λx. x (λy. y)就不會有歧義了。
這個規則是λ演算中函數應用的重點了。一句話來解釋就是,把參數應用到函數體中。舉一個例子:
有這么一個函數應用(λx.x)(λy.y),在這里把(λy.y)帶入前面函數的x中,就能得到最終的結果(λy.y),這里傳入一個函數,然后又返回一個函數,這就是最終的結果。
求值順序
考慮下面這個函數應用
(λ y. (λ x. x) y) E
有兩種計算方法,如下圖
可以先計算內層的函數調用再計算外層的函數調用,反之也可。
根據Church–Rosser定理,這兩種方法是等價的,最終會得到相等的結果,如上圖最后都得到了E。
但如果我們要自己實現一種語言,就有可能必選二選其一,于是有了下面兩種方式:
1,Call by Value(Eager Evaluation及早求值)
也就是上圖中的inner,這種方式在函數應用前,就計算函數參數的值。如:
(λy. (λx. x)y)((λu. u)(λv. v)) --->
(λy. (λx. x)y)(λv. v)--->
(λx. x)(λv. v)--->
λv. v
2,Call by Name (Lazy Evaluation惰性求值)
也就是上圖中的outer,這種方式在函數應用前,不計算函數參數的值,直到需要時才求值。如:
(λy. (λx. x)y)((λu. u)(λv. v)) --->
(λx. x)((λu. u)(λv. v)) --->
(λu. u)(λv. v)--->
λv. v
值得一提的是,Call by Name這種方式在我們目前的語言中,只有函數式語言支持。
λ演算與編程語言的關系
在λ演算中只有函數(變量依附于函數而有意義),如果要用純λ演算來實現一門編程語言的話,我們還需要一些數據類型,比如boolean、number、list等,那怎么辦呢?
λ的強大又再一次展現出來,所有的數據類型都能用函數模擬出來,秘訣就是
不要去關心數據的值是什么,重點是我們能對這個值做什么操作,然后我們用合法的λ表達式把這些操作表示出來即可。
聽上去很些云里霧里,但看了我下面的講解以后,你會發現,編程語言原來還可以這么玩,希望我能把這部分講清楚些,個人感覺這些東西太funny了 :-)
好了,我們先從最簡單——boolean的開始。
編碼Boolean
Ask:我們能對boolean值做什么?
Answer:我們能夠進行條件判斷,二選其一。
好,知道了能對boolean的操作,下面就用λ表達式來定義它:
true= λx. λy. x
false= λx. λy. y
if E1 then E2 else E3= E1 E2 E3
來簡單解釋一下,boolean就是這么一個函數,它有兩個參數(通過currying實現),返回其中一個。下面看個例子:
if true then u else v 可以寫成
(λx. λy. x) u v --->(λy. u) v --->u
哈哈,很神奇吧,更精彩的還在后頭呢,繼續
編碼pair
這里簡單解釋下pair,其實就是序列對,如(1 2)、(hello world),這些就是pair,只有兩個元素,但不要小看了pair,我們用的list就是通過pair連接起來形成的。
Ask:我們能對pair做什么?
Answer:我們能夠選擇pair中的任意一個元素
好,知道了能對pair的操作,下面就用λ表達式來定義它:
mkpair x y = λb. (b x y)
fstp=p true
sndp=p false
這里用到了true與false的編碼。解釋一下:
pair就是這么一個函數,參數是一個boolean值,根據這個參數確定返回值。還是看例子:
fst (mkpair x y)--->(mkpair x y) true ---> true x y--->x
這樣我們就能取到pair的第一個元素了。很好玩吧,下面的更有趣,繼續
編碼number
這里講的number是指的自然數。
Ask:我們能對number做什么?
Answer:我們能夠依次遍歷這些數字
好,知道了能對number的操作,下面就用λ表達式來定義它:
0 = λf. λs. s
1 = λf. λs. f s
2 = λf. λs. f (f s)
......
解釋一下,利用currying,我們知道上面的定義其實相當于一個具有兩個參數的函數:一個函數f,另一個是起始值s,然后不斷應用f實現遍歷數字的操作。先不要管為什么這么定義,看了下面我們如何定義加法乘法的例子你應該就會豁然開朗了:
首先我們需要定義一個后繼函數(The successor function)
succn= λf. λs. f (n f s)
然后,就可以定義加法與乘法了
add n1 n2=n1 succ n2
mult n1 n2=n1 (add? n2) 0
只看定義要想弄懂應該還是有些困難,下面看個具體的例子:
add0=(λn1. λn2. n1 succ n2)0// 這里直接根據上面 add 定義進行展開
-------將 0 帶入 n1,可得-------->
λn2. 0 succ? n2= λn2. (λf.? λs.? s)? succ? n2
-------將 succ 帶入 f,n2帶入 s,可得-->
λn2. n2= λx. x
我第一次看這個例子有個疑問,add不是兩個參數嗎,你怎么就加一個0呢?其實還是currying沒理解好,兩個參數的函數內部不也是用一個參數的函數來表示的嘛,如果只傳遞一個參數,那么我們就知道還會返回一個函數,本例中就是λx. x,這是恒等函數,也就是說加 0 ,相當于什么也沒加,還是本身。
哈哈,看來也不過如此嘛,如果你能看到這里,說明你已經對lambda掌握的差不多了。下面再來看個“難點”的例子——1+1:
add 1 1 --->
1 succ 1 --->
succ 1 --->
λf. λs. f (f? s) ---> 2
最后一個例子,2*2:
mult 2 2 --->
2 (add 2)? 0 --->
(add 2) ((add? 2)? 0) --->
2 succ(add? 2? 0) --->
2 succ(2? succ? 0) --->
succ( succ? (succ? (succ? 0))) --->
succ( succ ( succ ( λf. λs. f (0 f s)))) --->
succ( succ ( succ ( λf. λs. f s))) --->
succ ( succ ( λg. λy. g (( λf. λs. f s) g y)))
succ( succ (λg. λy. g (g y))) --->......---> λg. λy. g (g (g (g y))) =4
不要一看到這么多步驟就嚇跑了,原則很簡單,就是不斷進行函數應用,需要注意的就是這里的2、0不再是單純的數字了,它是從具有兩個參數的函數,如果你應用時只傳入一個參數,說明它還會返回一個函數。
不管怎樣,如果你已經看到了這里,我希望你能把上面這個乘法的例子看懂,就是不斷進行函數應用而已,沒什么東西,我覺得難點在于思維的轉化,因為以前都很理所當然認為2×2=4了,而不知道這么簡單的計算后面的本質性東西,通過這個例子,希望大家能明確一點:值是什么不重要,重要的是我們能對這個值進行的操作
最后再來一個收尾菜:
如果想要判斷一個數字是否為0,可以這么定義
iszero? n = n ( λb.? false)? true