大到能夠小說的Y組合子(三)

答:關於Fix的問題你fix了嗎?html

問:慢着,讓我想一想,上次留下個什麼問題來着?是說咱們有了一個求不動點的函數Fix,但Fix倒是顯式遞歸的,是吧?函數

答:有勞你還記的這個問題。spa

問:Fix的參與背離了匿名遞歸的定義,因此…因此…咱們被Fix給坑了?code

答:固然不是。你還記的第(一)章咱們討論過什麼嗎?htm

問:記的,咱們把一個顯式遞歸的Fact變成了一個匿名遞歸的結構。blog

答:很好,讓咱們再造一次輪子。遞歸

問:哦!我明白了,是用與上次相似的方法,把Fix寫成一個匿名遞歸的Lambda。ip

答:就是這個意思,如此即可用這個Lambda來得到不動點了。那就動手吧,讓咱們一路與Fact對比:get

//C#
//Func3 Fix = f=>f(Fix(x=>f(x)))
//寫成函數的形式爲:
Func1 Fix(Func2 f) { return (Func1)(f(x =>Fix(f)(x))); }
//對比Fact
int Fact(int x) { return x == 0 ? 1 : x * Fact(x - 1); }

 

//C# 
//爲了把本身傳給本身構造fix_maker
OuroborosFunc<Func3> fix_maker = 
    self => f => f(x => self(self)(f)(x));
//對比fact_maker
OuroborosFunc<Func<int, int>> fact_maker = 
    self => x => x == 0 ? 1 : x * self(self)(x - 1);

 

//C#
//自我調用產生Fix
Func3 Fix = ((Func<OuroborosFunc<Func3>, Func3>)(s => s(s)))
                            (self => f => f(x => self(self)(f)(x)));
//對比自我調用產生的Fact
Func<int, int> Fact = 
    ((Func<OuroborosFunc<Func<int, int>>, Func<int, int>>)(s => s(s)))
                            (self => x => x == 0 ? 1 : x * self(self)(x - 1));

觀察最後產生的Fix,發現咱們已經擺脫了顯式的遞歸調用,且獲得了一個可複用的不動點「生成器」。這裏插一句題外話,我以前按照上述思路獲得了Fix,後來才發現,原來這就是阿蘭•圖靈當年發現的Θ組合子(固然,我獲得這個組合子要容易得多,由於圖靈的年代根本沒有計算機,更別說程序語言了)。我相信這兩個組合子是徹底等價的,下一章咱們將以非形式化的方法來理解這一點。it

問:那麼,是時候揭開Y組合子的面紗了吧。

答:讓子彈再飛一下子。在此以前,「插播」一個小問題:若是遞歸函數的輸入參數不止一個,該怎麼辦呢?例如用展轉相除法求最大公約數:

//C#
//GCD的顯式遞歸
int GCD(int m, int n) { return n == 0 ? m : GCD(n, m % n); }

問:讓我想一想…嗯…那就要對以前定義的Y<T,TResult>作一些改變,寫成這樣:

//C#
class Y<T1, T2, TResult> 
{ 
        public delegate TResult Func1(T1 p1, T2 p2); 
        public delegate Func1 Func2(Func1 f1); 
        public delegate Func1 Func3(Func2 f2); 
        ... ... 
}

而後,對以前的Fix改寫成這樣:

//C#
Func3 Fix = ((Func<OuroborosFunc<Func3>, Func3>)(s => s(s)))
                            (self => f => f((x, y) => self(self)(f)(x, y)));

如此即可用這個Fix獲得gcd_seed的不動點,即GCD遞歸函數:

//C#
Y<int, int, int>.Func2 gcd_seed = gcd => (m, n) => n == 0 ? m : gcd(n, m % n);
Console.WriteLine(Y<int, int, int>.Fix(gcd_seed)(24, 15));    //3

答:很好,這是一個方案,但不是一個通用方案。若是有三個輸入參數的遞歸呢?四個呢?…

問:難道還有更好的方法嗎?

答:是的,答案就是Currying(柯里化)。所謂柯里化,就是把接受多個參數的函數變換成接受單一參數的函數,而且返回以柯里化的形式接受剩餘參數,最終返回結果的一種技術。(注:我是在獨立想到解決方案以後,才知道Currying的存在的哦!)

問:這…講得有點抽象…

答:那就直接上例子吧。在上述最大公約數的例子中,把GCD柯里化成以下形式:

//C#
Func<int, int> GCD(int m) { return n => n == 0 ? m : GCD(n)(m % n); }

即:高階函數GCD以m爲參數,返回一個能與m求最大公約數的函數,這個函數再受一個參數n,即可返回(m,n)的最大公約數。同時,Y<T, TResult>和Fix無需任何改變,便可完成有兩個輸入參數的遞歸:

//C#
Y<int, Func<int, int>>.Func2 gcd_seed = 
        gcd => m => n => n == 0 ? m : gcd(n)(m % n);
Console.WriteLine(Y<int, Func<int, int>>.Fix(gcd_seed)(24)(15));    //3

依此看來,兩個參數的Y<T, TResult>就足以知足任何遞歸的需求了,美哉!

問:然是美哉!

答:就在下章,靜候Y組合子的「粉墨登場」。待續…

相關文章
相關標籤/搜索