lambda演算簡單整理

最近在看虎書的時候見到了函數式語言,隨即想到了Lisp,和它的語言寫法——即(write (+ 1 2))這類東西。Lisp語言受到過λ演算的啓發,爲了更好的理解下函數式,便去查找了下λ演算的資料——但是網上資料比較少,折騰了幾日,把它們總結如下。

λ演算是阿隆佐邱奇(Alonzo Church)所發明的,那個時代還活躍着另一位科學家——阿蘭圖靈,就是發明圖靈機的那位。λ演算和圖靈機都做着相同的任務——計算,它是一個非常簡單而且小巧的形式系統,簡單理解,就像幾何原本裏面的幾條公理,通過那幾條公理,就像符號遊戲一般,推導出整本書的所有定理、假設等等。這種有公理和能夠進行推導的就是形式系統,自然地,描述它的就是形式語言。

在λ演算看來,一切都是表達式,包括函數——函數是抽象的表達式。它有三種合法的表達式,或者叫做項:

λ-term: 變量 Variable

首先是變量,是一個標識符,比如x、xy、var這些。注意,不要把變量理解爲一個值,它可以代表任何東西,比如你認爲平行線段永不相交,那麼它就可以當作一個變量,並通過一系列操作推導出其它定理,當然,我們也可以用它表示一個值。

變量有着和一般程序語言類似的規則。當一個變量是在當前作用域下定義的,那麼稱它是綁定的(類比於局部變量),當一個變量是外層作用域定義的,但是本層作用域訪問了,那麼該變量是自由變量(類比逃逸變量)。注意,自由變量還是綁定變量是相對於作用域而言的——對於內層作用域,一個外層作用域的變量是自由的,而外層作用域則認爲它是綁定的。

一個不含自由變量的項被稱作封閉的,或者稱爲組合子。

λ-term: 抽象體 Abstraction

抽象體類似於函數一樣,表達着擁有一個參數(只能是一個參數)的函數,以及它的返回值。例如λx.M,就表示參數爲x,返回值爲M。再來幾個例子:λx.x 表示輸入x就返回x,也就是輸入什麼就返回什麼。λx.y,表示輸入什麼都返回y。可以對比一下這個圖來理解,第二行是一些函數式語言的寫法,第三行是JavaScript的樣貌。

類似於高階函數的概念,一個抽象體的輸入x當然也可以是一個抽象體,自然的,表達式M的結果也可以是抽象體。

抽象體其實就是函數的一種抽象。下面我更多的使用我們熟悉的"函數"一詞來替代它。

λ-term: 應用 Application

其實想像成調用會更加的形象。M N表示傳入參數N,調用M。例如(λx.x) p,應該得到的結果是p——函數的輸入參數是x,表達式的結果也是x,也就是一個輸入什麼就返回什麼的函數,現在我們送入變量p,自然得到的也是p。同樣的,我們可以類比下圖,第二段是很多程序語言的函數調用寫法:

我們再來一個例子:(λx.y)((λx.y) p)得到的結果是y,第一個應用是內層的,它得到y,第二個應用返回自身,得到的還是y。

消除歧義

爲了避免歧義,有兩條約定規則:

1. 函數會儘可能的往右拓展,例如λx.y z表達的是(λx.(yz)),而不是(λx.y) z

2. 從左往右查看它們

其餘情況我們就使用小括號闡明優先級了,就像平時那樣。

多參數函數: 柯里化 Currying

一個函數需要好幾個參數非常正常的。但是我們之前約束了只能有一個參數——怎麼辦?

爲了方便起見,我使用類似JavaScript的語法解釋。例如這個函數:

function F(x, y)
{
    return Express;
}

這裏有個多參數函數,它有兩個參數。那麼我們要把它變成一個參數的怎麼處理呢,可以想到的方案之一就是:

function F(x)
{
    return function(y)
    {
        return Express;
    }
}

套層娃。翻譯成λ演算的表達:λx.(λy.xy),xy是我們的表達式,而y是我們的內層函數的變量,x是自由的,即外層函數作用域下的變量。如此一來,我們第一步返回的是一個函數,它只有一個參數y,內層是我們的表達式,接着,我們再調用一次這個返回的函數,得到最終結果:

Result = F(x) (y);

能夠返回函數的函數也是函數式語言的關鍵點:高階函數。

函數內聯: α轉換

爲了方便理解,這裏我選用C/C++語言作爲例子。考慮下面的一個片段,我們手工完成一次函數內聯。

inline int f(int x)
{
    int y = x;
    return y + 1;
}

int y;
void Apply()
{
    x = f(5);
    y = 8;
}

如果我們傻傻的直接拷貝過來,名字也不改,程序就變成了這樣子:

int y;
void Apply()
{
    int y;
    y = 5;
    x = y + 1;
    y = 8;
}

很顯然這是不可以的(該情況被稱作變量捕獲,即外層變量名被內層變量名覆蓋,y=8不再是全局的y)。注意到,一個程序的運行情況並不會和變量名、函數名有什麼關係,x = y * 2和x_pos = y_pos * 2在做的都是同樣的任務——取得一個變量值,乘以2,儲存到另一個變量中。當然,有例外情況,當一個變量a是自由的,那麼它的名字不能隨意更改,不然就無法對應到正確的位置了。因此,總結一下——一個被綁定的變量名字,並不會影響函數本身,更換另一個名字之後是等價的

上面這條就是所謂的α轉換。如果我們用λ演算式寫一下,可以這樣表達:

λx.x = λy.y

函數調用: β歸約

計算λ式子就是歸約(reduce,刻意強調下這個詞)過程。例如(λx.E) p,我們將p代入,得到E。將它表述出來就是:

(λx.E) p = E [p / x] = E [x := p]

上面就是β歸約——將E自由變量x(相對於E,x是自由變量)替換爲p。一些地方會使用中間的表達,一些地方使用後面的表達。個人覺得後面的比較清晰,所以用後面的來表達。

我們來理解一下它,用一個類似JavaScript的例子:

function funA(a)
{
    return (a + 2);
}

用並不嚴格的λ式寫法,它相當於λa.(a + 2)。現在我們調用funA(5),即:

(λa.(a + 2)) 5

很顯然,相對於a + 2,變量a是自由變量,因此將a替換爲5:

5 + 2

可以看出,我們得到了想要的結果:7。接下來我們看一個特殊的例子:

(λx.(λy.xy))  y

我們進行代換,針對於(λy.xy),x是自由變量:

(λy.xy) [ x:= y]

針對於xy,x依然是自由變量,我們簡單的把它放進去,得到:λy.yy。完蛋。變量名錯了。這就是之前提到的內聯問題,外層變量被內層的覆蓋掉了。所以我們要適當的時候α轉換變換一下。我們可以把它換成y',得到:λy.y'y,這樣就是正確的了。

冗餘消除: η歸約

這個其實很簡單,考慮下面這個JavaScript片段

function(x)
{
    return y;
}

我們無論送入任何樣貌的參數x,都沒法改變返回值。所以說,(λx.y) x是可以直接被替換爲y的。概念化一些——對於λx.E,如果E不含綁定變量x,那麼該式子恆等於E。這就是η歸約

參考資料

除了網絡上各個大佬的專欄、博客外,還有:http://users.monash.edu/~lloyd/tildeFP/Lambda/Ch/01.Calc.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章