心結終解:Y組合子工程推導全過程!

緣起

第一次聽說Y組合子,大概是在19年的時候。當時看到這麼個東西的時候,就覺得很漂亮。然後,也不知道薅掉多少根頭髮,終於在最近頓悟了其中的關鍵步驟,遂把思路整理成文章記錄下來。

先簡單說一下Y組合子產生的背景吧。

上個世紀三十年代,丘奇發明了Lambda演算(它是後來很多函數式變成的理論基礎)。大概是因爲信奉如無必要勿增實體,當年老爺子提出的理論裏,函數是單參的、也沒有名字的概念。

之後,柯里先救了一次場,證明多參函數可以用單參函數等價表示,這就是函數式編程裏大名鼎鼎的柯里化。

但是沒有函數名,怎麼實現遞歸呢?關鍵時刻,柯里大神再次救場,證明了不需要名字,也能實現函數的遞歸。

不過,純數學的理論證明咱也不會啊,所以,下面就用Scheme語言(基於Lambda的一種Lisp方言)以工程的視角推導出一個Y組合子。

推導過程

(define fact
  (lambda (n)
    (if (< n 2) 1 (* n (fact (- n 1)))))
)

這是一個遞歸求階乘的函數。剛纔說了,Lambda演算不能存在函數名,也就是說不能用define定義fact。但是,這裏其實有一個變通方案:不能定義函數名,但是可以給變量命名,比如n。所以,第一步,我們把fact作爲變量傳進來。

(define some-name
  (lambda (fact)
    (lambda (n)
      (if (< n 2) 1 (* n (fact (- n 1))))))
)
((some-name 'null) 1)
((some-name (some-name 'null)) 2)
((some-name (some-name (some-name 'null))) 3)

針對第一個式子,其實fact傳入的是什麼東西都無所謂,因爲n = 1,所以不會走到(fact 0),否則fact帶入'null,會報錯

針對第二個式子,n = 2時,帶入得到 (* 2 (...)),其中...的部分就是第一個式子的內容,即((some-name 'null) 1)

針對第三個式子,n = 3時,帶入得到 (* 3 (...)),其中...的部分就是第二個式子的內容,即((some-name (some-name 'null)) 2)

其實可以看出來,只要最後的(fact 0)不要真的執行到,就可以算越來越大的n

但是,我們不想 n = 4式,寫這麼長的式子了,((some-name (some-name (some-name (some-name 'null)))) 4)

所以我們試着做以下這2個替換:

第一步:既然'null都可以讓程序跑起來,那替換成some-name是不是也可以?

((some-name some-name) 1)
((some-name (some-name some-name)) 2)
((some-name (some-name (some-name some-name))) 3)

第二步:既然程序最後終止在(fact 0),用some-name帶入fact後,實際上是終止在(some-name 0)

那是不是把(fact 0)改寫成((fact fact) 0),程序就不會終止了?

(define some-name
  (lambda (fact)
    (lambda (n)
      (if (< n 2) 1 (* n ((fact fact) (- n 1))))))
)
((some-name some-name) 1)

((some-name some-name) 2)
; = (* 2 ((some-name some-name) 1))

((some-name some-name) 3)
; = (* 3 ((some-name some-name) 2))
; = (* 3 (* 2 ((some-name some-name) 1)))

((some-name some-name) 4)
; = (* 4 ((some-name some-name) 3))
; = (* 4 (* 3 ((some-name some-name) 2)))
; = (* 4 (* 3 (* 2 ((some-name some-name) 1))))
(((lambda (g) (g g)) some-name) 4)

可以看到這時候,其實要不要some-name已經沒有關係了,完全可以把some-name用它真正的定義塞進去

(
  ; 這其實是一個函數,接受一個參數n,計算n的階乘
  (
    (lambda (g) (g g))
    ; 這其實是剛纔的some-name
    (lambda (fact)
      (lambda (n)
        (if (< n 2) 1 (* n ((fact fact) (- n 1))))))

  )
4)

上面的那個函數成爲“窮人的Y組合子”,因爲它指針對特定的遞歸函數生效,在這個例子裏是fact

我們希望把fact提出去,讓這個函數更通用一些

首先把fact代換爲f(這一步實際上只是爲了好看)

(
  ; 這其實是一個函數,接受一個參數n,計算n的階乘
  (
    (lambda (g) (g g))
    ; 這其實是剛纔的some-name
    (lambda (f)
      (lambda (n)
        (if (< n 2) 1 (* n ((f f) (- n 1))))))

  )
4)

接着我們把(f f)提出去

(
  (
    (lambda (g) (g g))
    
    (lambda (f)
      (
        ; 這是最初的階乘函數
        (lambda (fact) 
          (lambda (n)
            (if (< n 2) 1 (* n (fact (- n 1))))))
        ; 用lambda包一下,本質上就是剛纔的 (f f)
        ; 這一步主要是因爲Scheme是應用序求值
        ; 因此,如果不用lambda讓它延遲求值的話,就會提前遞歸下去
        (lambda (x) ((f f) x))
      )
    )
  )
4)

再把fact相關的提出去

(define some-name
  (lambda (fact)
    (lambda (n)
      (if (< n 2) 1 (* n (fact (- n 1))))))
)

(
  (
    (lambda (g) (g g))
    (lambda (f) (some-name (lambda (x) ((f f) x))))
  )
4)
(
  (
    ; This is Y !!!
    (lambda (fn)
      (
        (lambda (g) (g g))
        (lambda (f) (fn (lambda (x) ((f f) x))))
      )
    )
    some-name
  )
4)

最終我們得到Y組合子

(define Y
  (lambda (fn)
    ((lambda (g) (g g))
    (lambda (f) (fn (lambda (x) ((f f) x)))))))

拿階乘函數先測試下

((Y some-name) 5)

試試斐波那契數列

(define meta-fib
  (lambda (fib)
    (lambda (n)
      (if (< n 3) 1 (+ (fib (- n 1)) (fib (- n 2)))))))

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