新聞中心
前言

這是一個比較古老的話題,三年半之前,老趙就此寫過一篇很文章《使用Lambda表達式編寫遞歸函數(shù)》。其中提出了偽遞歸的概念,提出了自己的解決方式,也引出了裝配腦袋 使用不動點組合子 的解決辦法。此后好長一段時間,偽遞歸和不動點組合子成了兩個園子里的兩大熱門話題。
當年我也寫了篇文章《反駁 老趙 之 “偽”遞歸》參與了爭論,不過對老趙提出的解決方式及裝配腦袋的不動點組合子思路,一直沒弄清楚。中間一段時間,工作忙,忘卻了。
最近比較輕閑,靜下心來學習了下相關(guān)的的一些理論,并深入思考,略有所悟,在此和大家分享下。
本文及后續(xù)章節(jié)會用到相當復雜的泛型及 lambda 表達式,請做好相關(guān)技術(shù)和心理準備。
使用 Lambda 表達式構(gòu)建遞歸函數(shù)
很多朋友認為這很容易,隨手便可用 lambda 表達式寫出一個階乘遞歸:
- Func
fact = x => x <= 1 ? 1 : x * fact(x - 1);
不過,很抱歉,這行代碼是無法通過編譯的,VS 提示:使用了未賦值的變量 fact。
有種簡單的解決辦法,把上面這行代碼拆成兩行:
- Func
fact = null; - fact = x => x <= 1 ? 1 : x * fact(x - 1);
不過這種寫法也有問題,老趙說得比較清楚,我就不在贅述了,請查看《使用Lambda表達式編寫遞歸函數(shù)》一文中偽遞歸部分。
那么如何解決 lambda 表達式構(gòu)建遞歸函數(shù)的問題呢?根據(jù)函數(shù)式編程理論,我們可以使用不定點組合子。
在學習不定點組合子之前,需要先了解更基礎(chǔ) λ 演算。
λ 演算
λ 演算的基礎(chǔ)請大家參考維基百科:
http://zh.wikipedia.org/wiki/Lambda_演算
http://en.wikipedia.org/wiki/Lambda_calculus
非形式化的描述
請確保你已經(jīng)理解了文中幾個表達式的等價關(guān)系:
- (λf. f 3)(λx. x + 2) == (λx. x + 2) 3 == 3 + 2
- (λx. λy. x - y) 7 2 == (λy.7 - y) 2 == 7 - 2
還清楚知道函數(shù)應用(application)的概念及其左結(jié)合性:
- f x y == (f x) y
還有它的各種等價變換
- f x y == (f x) y = (f(x))y = (f(x))(y) = f(x)(y)
歸約
并會運用三個常用的規(guī)約(Reduction)
1.α-變換(α-conversion)
2.β-歸約(β-reduction)
3.η-變換(η-conversion)
不動點組合子
請參考:
http://zh.wikipedia.org/wiki/不動點組合子
http://en.wikipedia.org/wiki/Fixed-point_combinator
定義
不動點組合子(Fixed-point combinator,或不動點算子,使用 FIX 表示)是計算其他函數(shù)的一個不動點的高階函數(shù)。
不動點算子具有以下特性,對于任何函數(shù) f 都有:
- FIX ff = f (FIX f)
定義匿名的遞歸函數(shù)
不動點組合子允許定義匿名的遞歸函數(shù),具體來說是將一個非遞歸的單步函數(shù)(只執(zhí)行遞歸中的一步,a single step of this recursion,使用 g 表示)轉(zhuǎn)換為遞歸函數(shù)。
如下是階乘的單步函數(shù)定義:
- g = λf. λn. (ISZERO n) 1 (MULT n (f (PRED n)))
FIX g 可獲取到匿名的遞歸函數(shù):
- FIX g = λn. (ISZERO n) 1 (MULT n ((FIX g) (PRED n)))
向 FIX g 的參數(shù) n 傳入值 5,可最終得出:
- FIX g 55 = 5 * (4 * (3 * (2 * (1 * 1)))) = 120
常用的不定點組合子
不動點組合子中最有名的(也可能是最簡單的)是 Y 組合子:
- Y = λf. (λx. f (x x)) (λx. f (x x))
另一個常見不動點組合子是圖靈不動點組合子(阿蘭·圖靈發(fā)現(xiàn)的):
- Θ = (λx. λy. (y (x x y))) (λx.λy.(y (x x y)))
傳值調(diào)用(call-by-value)
在 λ 演算中,每個表達式(lambda term)都代表一個只有單獨參數(shù)的函數(shù),這個函數(shù)的參數(shù)本身也是一個只有單一參數(shù)的函數(shù),同時,函數(shù)的值是又一個只有單一參數(shù)的函數(shù)。
根據(jù)此描述,可知 λ 演算中函數(shù)只有一個參數(shù),這個參數(shù)是一個函數(shù),而不是一個值。而對于我們常見的遞歸(階乘、斐波那契數(shù)列求值),參數(shù)都是值(自然數(shù))。兩者是不匹配的。
為了適當傳值調(diào)用,需要將不動點組合子 η-展開:
簡單而言 η-變換 是說 λx. f x 和 f 可以互相轉(zhuǎn)換。從 f 這種簡單形式 η-變換 為 λx. f x 復雜形式,稱為 η-展開。
對于 Y 組合子,通常是將其中的 (x x) η-展開為 λy. x x y,由此得出傳值調(diào)用版本的 Y組合子(也稱為 Z 組合子):
- Y = λf. (λx. f (λy. x x y)) (λx. f (λy. x x y))
如果不展開呢?會怎樣?
如果使用不展開的不動點算子,也能寫出可編譯通過的代碼,但最終執(zhí)行會陷入死循環(huán),直至堆棧溢出。
小結(jié)
后續(xù)章節(jié)將使用以下符號和名稱,不再另行說明:
1.FIX:不動點組合子
2.g:單步函數(shù)
3.n:表示遞歸函數(shù)的參數(shù)(在階乘、斐波那契數(shù)列求值中是一個自然數(shù))
對于 FIX、g、n:
1.FIX g: 將會生成對應的遞歸函數(shù)
2.FIX g n: 將進行遞歸運算
λ 演算表達式與 c# lambda 表達式的對應關(guān)系
λx. x + 2
λx. x + 2 在 c#中的 lambda 表達式可表式為:x => x+ 2;
假定 x 的 int 類型,可寫作:
- Func
f = x => x + 2;
相應 (λx. x + 2) 1 可寫為:
- var result = f(1); // 結(jié)果為 3
λx. λy. x + y
復雜點,λx. λy. x + y 用 c# 的 lambda 表達式表示為:x => y => x + y;
x, y 類型為均整數(shù)時,可寫作:
- Func
> f = x => y => x + y;
相應 (λx. λy. x + y) 1 2 便是:
- var result = f(1)(2); // 結(jié)果為 3
λx. λy. λz. x + y + z
再復雜些,λx. λy. λz. x + y + z 表示為:x => y=> z => x + y + z,三個參數(shù)都為 int 時 c# 代碼:
- Func
>> f = x => y => z => x + y + z;
可如下調(diào)用:
- // (λx. λy. λz. x + y + z) 1 2 3
- var result1 = f(1)(2)(3); //結(jié)果為 6
- // (λx. λy. λz. x + y + z) 1 → λy. λz. 1 + y + z
- Func
> g = f(1); - // (λy. λz. 1 + y + z) 2 → λz. 1 + 2 + z → λz. 3 + z
- Func
h = g(2); - // (λz. 3 + z) 3 → 3 + 3 → 6
- var result2 = h(3); // 結(jié)果為 6
每 5 行,向 f 傳入一個常量 1,返回一個新的方法 g;再經(jīng)過第 7 行,向 g 傳入常量 2,再次返回一個新方法 h。
方法 h 只能接受一個參數(shù),***得出 h(3) = 6。
為什么不是 (x, y, z) => x + y + z?
也許你會有疑問,不就是 x、y、z 三個整數(shù)加起來嘛,為什么搞這么復雜,像下面這樣不是更簡單嗎?
- Func
f = (x, y, z) => x + y + z; - var result = f(1, 2, 3);
確實簡單,不過:
在 λ 演算中,每個表達式(lambda term)都代表一個只有單獨參數(shù)的函數(shù),這個函數(shù)的參數(shù)本身也是一個只有單一參數(shù)的函數(shù),同時,函數(shù)的值是又一個只有單一參數(shù)的函數(shù)。
注意都是只有一個參數(shù),對應到 c# 的 lambda 表達式,也應是一個參數(shù),所以是:x => y=> z => x + y + z。
總結(jié)
| λ 演算表達式 | c# lambda 表達式 |
| λx. x + 2 | x => x+ 2 |
| λx. λy. x + y | x => y => x + y |
| λx. λy. λz. x + y + z | x => y=> z => x + y + z |
好像有些規(guī)律:對于一個 Lambda terms,去掉“λ”并把“.”替換為”=>”便可變成對應 lambda 表達式。(注意,這個規(guī)律不嚴謹?。?/p>
練習一下,看看下面這個如何轉(zhuǎn)為 lambda 表達式:
- λx. λn. (g (x x) n)
先對它進行一步演算得出:
- λx. λn. (g (x(x)) (n))
運用上的的規(guī)律,可以寫出 lambda 表達式:x => n => g((x(x))(n)
對于復雜點的如:λx. f ( λv. (x x) v),這條規(guī)律就不適用了。文后續(xù)部分會通過演算繞開這種復雜的轉(zhuǎn)換,不對此進行討論。
理解本文中的泛型和 lambda 表達式
對于上一部分使用的泛型和 lambda 表達式,尤其是下面這行代碼,你需要花點時間去理理思路(因為后續(xù)章節(jié)中泛型要遠比此復雜):
- Func
>> f = x => y => z => x + y + z;
如果對你對泛型和 lambda 認識不是非常深刻的話,難度有點大,不妨先從下面這個簡單點的開始:
- Func
> f = x => y => x + y;
換種寫法,或許有助于理解:
- Func
> f = x => { - Func
g = y => { return x + y ;}; - return g;
- };
本文簡單闡述了 lambda 構(gòu)建遞歸函數(shù)的問題,粗略提及 λ 演算及不動點組合子的知識,并總結(jié)了下 λ 演算表達式與 c# lambda 表達式的對應關(guān)系。
分享標題:使用Lambda表達式編寫遞歸一:前言及基礎(chǔ)
瀏覽路徑:http://m.fisionsoft.com.cn/article/codpeso.html


咨詢
建站咨詢
