C# 函數式編程 —— 使用 Lambda 表達式編寫遞歸函數

最近看了趙姐夫的這篇博客http://blog.zhaojie.me/2009/08/recursive-lambda-expressions.html,主要講的是如何使用 Lambda 編寫遞歸函數。其中提到了不動點組合子這個東西,這個概念來自於函數式編程的世界,直接理解起來可能有些困難,因此咱們能夠一塊兒來嘗試使用 Lambda 來編寫遞歸函數,以此來探索不動點組合子的奧祕。在閱讀過程當中,咱們可使用「C# 交互窗口」或者 Xamarin WorkBook 來運行給出的代碼,由於 Lambda 表達式中的變量,類型大多會被省略掉,直接閱讀起來可能有些難懂。html

首先用常規手段寫一個遞歸形式的階乘express

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 中調用。post

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 函數就變得簡短了不少,可是其中仍引用了一個在外部定義的函數,這讓他變得不夠「」,因此能夠把這個函數做爲參數傳入.net

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 啦~code

F sF = f => f(f);

sF(newFac(fac0))(5)
// 120

如今接着嘗試把fac0從兩層括號中解放出來,以實現柯里化。因此首先就須要定義一個接受跟newFac類型相同的委託做爲參數,並返回一個委託,這個返回的委託接受一個參數,參數類型與 fac0 相同。htm

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 能夠代換爲 newFacnewFac(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),若是咱們把 facnewFac0 表示的 Lambda 表達式叫作 fn(f),其中 f = fn(f),這裏出現了遞歸的定義,畢竟 fac 表示的是一個遞歸函數。也就是說 ffn 這個函數映射到了自身,這在數學上叫作「不動點」,例如 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/

相關文章
相關標籤/搜索