最近看了趙姐夫的這篇博客http://blog.zhaojie.me/2009/08/recursive-lambda-expressions.html,主要講的是如何使用 Lambda 編寫遞歸函數。其中提到了不動點組合子這個東西,這個概念來自於函數式編程的世界,直接理解起來可能有些困難,所以我們可以一起來嘗試使用 Lambda 來編寫遞歸函數,以此來探索不動點組合子的奧秘。在閱讀過程中,我們可以使用“C# 交互窗口”或者 Xamarin WorkBook 來運行給出的代碼,因為 Lambda 表達式中的變量,類型大多會被省略掉,直接閱讀起來可能有些難懂。
首先用常規手段寫一個遞歸形式的階乘
int facRec (int n)
{
return n == 1 ? 1 : n * facRec(n - 1);
}
facRec(5)
// 120
那么如何使用 Lambda 表示階乘的遞歸形式呢?Lambda 是匿名函數,那么就不能直接在內部調用自己,不過函數的參數是可以有名字的,那么可以給這個 Lambda 添加一個函數參數,在調用的時候,就把這個 Lambda 自己作為參數傳入,從而實現遞歸的效果:
delegate Func<int, int> F(F self);
F fac = (F f) => (int n) => n == 1 ? 1 : n * f(f)(n - 1);
fac(fac)(5)
// 120
您可能已經發現了,我沒有把 F 定義為接受兩個參數,第一個接受一個函數作為參數,第二個是要求階乘的值,返回一個 int 結果的形式。這其實是一種函數式編程的做法——任何包含多個參數的函數都可以寫成多個只包含一個參數的函數的組合的形式,我們把這種操作叫做“柯里化”,例如:
int sum(int a, int b, int c)
{
return a + b +c;
}
Func<int, Func<int ,int>> fSum(int a)
{
return (int b) =>
{
return (int c) =>
{
return a + b + c;
};
};
}
sum(1,2,3) == fSum(1)(2)(3)
//true
雖然fSum的返回值類型看起來有些鬼畜,但是完全是 C# 自己的原因——不能自動推斷方法的返回值類型。
接着回到我們的探索過程,注意到第3行出現了f(f)
這樣的東西,那么可以把這種表達式提取出來,作為參數傳入。
fac = (F f) => (int n) =>
{
Func<Func<int,int>, Func<int,int>> tmp = (Func<int,int> g) =>
{
return (int h) =>
{
if(h == 1)
return 1;
else
{
return h * g(h - 1);
}
};
};
return tmp(f(f))(n);
};
fac(fac)(5)
// 120
現在,可以看到第 5 行返回的函數看起來挺像我們最開始定義的普通形式的遞歸階乘,何不嘗試將其提取出來,然后在 fac 中調用。
Func<Func<int, int>, Func<int, int>> fac0 = (Func<int, int> g) =>
{
return (int h) =>
{
if(h == 1)
return 1;
else
{
return h * g(h - 1);
}
};
};
fac = (F f) => (int n) =>
{
return fac0(f(f))(n);
};
fac(fac)(5)
// 120
這下我們的 fac 函數就變得簡短了很多,但是其中仍引用了一個在外部定義的函數,這讓他變得不夠“純”,所以可以把這個函數作為參數傳入
delegate F NewF(Func<Func<int, int>, Func<int, int>> g);
NewF newFac = g =>
{
return (F f) => (int n) => g( f(f) )(n);
};
// 等價於
newFac = g => f => n => g(f(f))(n);
newFac(fac0)(newFac(fac0))(5)
重復的東西又出現了,可以把newFac(fac0)
提取出來,這樣的話就需要一個接受 F
類型函數並返回一個 Func<int, int>
類型函數的東西——其實就是前面定義的 F
啦~
F sF = f => f(f);
sF(newFac(fac0))(5)
// 120
現在接着嘗試把fac0
從兩層括號中解放出來,以實現柯里化。所以首先就需要定義一個接受跟newFac
類型相同的委托作為參數,並返回一個委托,這個返回的委托接受一個參數,參數類型與 fac0
相同。
delegate Func<Func<Func<int, int>, Func<int, int>>, Func<int,int>> NewSF(NewF newF);
NewSF newSF = newF =>
{
return (Func<Func<int, int>, Func<int, int>> g) =>
{
var f = newF(g);
return f(f);
};
};
newSF(newFac)(fac0)(5)
newF
是一個 NewF
類型的委托,返回值的類型是 F
。注意到 newFac = g => f => n => g(f(f))(n)
,這是一個純函數,可以直接代入到newSF
之中,所以上面的newSF
可以進一步化簡。首先用泛型化簡 g
的類型,在泛型的特例化之后,g
的類型跟上面的 newSF
里面的 g
的類型其實是一樣的。newSF
的參數 newF
可以代換為 newFac
,newFac(g)
的結果類型是 F
,也就是上面的 f
,因為 f
需要把自身作為參數,所以就重新把 newFac(g)
作為參數傳給 newFac(g)
返回的委托。
delegate T Y<T>(Func<T, T> g);
Y<Func<int, int>> y = g =>
{
return n =>
{
return newFac(g)(newFac(g))(n);
};
};
y(fac0)(5)
// 120
還記得我們得出 sF
的過程嗎?接着把上面的 y
化簡一下
y = g =>
{
return n =>
{
return sF(newFac(g))(n);
};
};
y(fac0)(5)
// 120
然后寫的緊湊一些
y = g => n => sF(newFac(g))(n);
看看我們現在得到的成果:
sF = f => f(f);
newFac = g => f => n => g(f(f))(n);
y = g => n => sF(newFac(g))(n);
y(fac0)(5)
由於 C# 並不是一門函數式的語言,Lambda 表達式不能直接調用,必須要轉換成委托類型才可以直接調用,所以導致了 y
函數依賴另外兩個函數,不過由於依賴的兩個函數都是純函數,所以沒啥影響。但是上面的式子仍可繼續簡化,下面我把 newFac
定義在 y 表達式的內部:
y = g =>
{
return n =>
{
NewF localNewFac = localG => f => localN => localG(f(f))(localN);
return sF(localNewFac(g))(n);
};
};
y(fac0)(5)
可以看到 localNewFac
接受一個 localG
作為參數,然后返回一個 lambda 表達式,然后在第6行把 g
作為了實參傳遞給 localNewFac
,這么看來,localNewFac
其實沒必要接受一個 localG
作為參數,只要在閉包中捕獲外部的變量 g
就好了
y = g =>
{
return n =>
{
F localF = f => localN => g(f(f))(localN);
return sF(localF)(n);
};
};
y(fac0)(5)
由於有 sF
的存在,編譯器就有能力推斷 sF
的參數類型,上面的代碼就可以簡化為:
y = g =>
{
return n =>
{
return sF(f => localN => g(f(f))(localN))(n);
};
};
y(fac0)(5)
現在,我們就可以得到下面兩個式子:
sF = f => f(f);
y = g => n => sF (f => m => g(f(f)) (m)) (n);
y(fac0)(5)
// 120
現在來重新審視一下 fac0
的類型,可以將其定義為下面的樣子
delegate T FT<T>(T f);
FT<Func<int, int>> newFac0 = (Func<int, int> f) => n => n == 1 ? 1 : n * f(n - 1);
忽略類型不看的話,這個 newFac0
跟最開始定義的 fac
簡直一模一樣!接下來就重新定義一下 Y
的類型,使其能與 FT
類型兼容:
delegate T YT<T> (FT<T> f);
delegate T SFT<T> (SFT<T> f);
SFT<Func<int, int>> sFT = f => f(f);
YT<Func<int, int>> yt = g => n => sFT (f => m => g(f(f)) (m)) (n);
yt(newFac0)(5)
// 120
SFT
是一個輔助類型,因為 C# 里面不能直接調用 f => f(f)
這樣的表達式。FT
是一個泛型的遞歸表達式的類型,可以用來定義任意的有遞歸能力的 Lambda。YT
定義了一個高階函數的類型,可以用來遞歸調用一個匿名函數:
yt(f => n => n == 1 ? 1 : n * f(n - 1))(5)
再回過頭去看最開始 fac
的使用方式: fac(fac)(5)
,如果我們把 fac
跟 newFac0
表示的 Lambda 表達式叫做 fn(f)
,其中 f = fn(f)
,這里出現了遞歸的定義,畢竟 fac
表示的是一個遞歸函數。也就是說 f
被 fn
這個函數映射到了自身,這在數學上叫做“不動點”,例如 f(x) = x^2
, 那么 x = 1
時,f(1) = 1
,那么 x
就是函數 f
的一個不動點。
所以 yt(fn(f)) = fn(fn(f)) = fn(f) = f
好吧,其實這里我也有些混亂了
所以 yt(fn)
這個函數計算出了函數 fn(x)
一個不動點,也就是 f
,人們就把 yt
稱為 不動點算子(factor) 也就是 Y Combinator。
參考鏈接:
https://blog.cassite.net/2017/09/09/y-combinator-derivation/