如何實現一個沒有名字的遞歸函數

本文原發於我的博客html

遞歸 做爲計算機科學中很重要的一個概念,應用範圍很是普遍。比較重要的數據結構,像樹、圖,自己就是遞歸定義的。
比較常見的遞歸算法有階乘斐波那契數等,它們都是在定義函數的同時又引用自己,對於初學者來講也比較好理解,可是若是你對編程語言,特別是函數式語言,有所研究,可能就會有下面的疑問:算法

一個函數在尚未定義完整時,爲何可以直接調用的呢?編程

這篇文章主要是解答上面這個問題。閱讀下面的內容,你須要有些函數式編程的經驗,爲了保證你可以比較愉快的閱讀本文,你至少能看懂前綴表達式。相信讀完本文後,你將會對編程語言有一全新的認識。
本文全部演示代碼有SchemeJS兩個版本。數據結構

問題描述

下面的講解以階乘爲例子:編程語言

; Scheme
(define (FACT n)
  (if (= n 0)
    1
    (* n (FACT (- n 1)))))

; JS
var FACT = function(n) {
    if (n == 0) {
        return 1;
    } else {
        return n * FACT(n-1);
    }
}

上面的階乘算法比較直觀,這裏就再也不進行解釋了。重申下咱們要探究的問題函數式編程

FACT 這個函數爲何在沒有被定義完整時,就能夠調用了。函數

問題分析

解決一個新問題,常見的作法就是類比以前解決的問題。咱們要解決的這個問題和求解下面的等式很相似:.net

2x = x + 1

在等號兩邊都出現了x,要想解決這個問題,最簡單的方式就是將等號右邊的x移到左邊便可。這樣就知道x是什麼值了。code

可是咱們的問題比這個要複雜些了,由於咱們這裏須要用ifn*-這四個符號來表示FACT,能夠這麼類比是由於一個程序無非就是經過一些具備特定語意的符號(編程語言規定)構成的。orm

再進一步思考,FACT 須要用四個符號來表示,這和咱們求解多元方程組的解不是很像嘛:

x + y = 3
x - y = 1

爲了求解上面方程組,通常能夠轉爲下面的形式:

x = 3 - y
y = x - 1

(x, y) = T (x, y)

其中的T爲一個轉換,在線性代數其實就是個矩陣,根據矩陣T的一些性質,咱們能夠斷定(x ,y)是否有解,以及解的個數。

對比此,咱們能夠把問題轉化爲下面的形式:

FACT = F (FACT)

上面的F爲某種轉換,在這裏其實就是個須要一個函數做爲參數而且返回一個函數的函數。若是存在這麼個F函數,那麼咱們就能夠經過求解F不動點來求出FACT了。

但這裏有個問題,即使咱們知道了F的存在,咱們也沒法肯定其是否存在不動點,以及若是存在,不動點的個數又是多少?

計算機科學並不像數學領域有那麼多能夠套用的定理。

尋找轉換函數 F

證實F是否存在是個比較難的問題,不在本文的討論範圍內,這涉及到Denotational semantics領域的知識,感興趣的讀者能夠本身去網上查找相關資料。

這裏直接給出FACT對應的函數F的定義:

; Scheme
(define F
  (lambda (g)
    (lambda (n)
      (if (= n 0)
        1
        (* n (g (- n 1)))))))

; JS
var F = function(g) {
    return function(n) {
        if (n == 0) {
            return 1;
        } else {
            return x * g(n-1);
        }
  }
}

能夠看到,對比遞歸版本的FACT變更不大,就是把函數內FACT的調用換成了參數g而已,其實咱們常見的遞歸算法均可以這麼作。

尋找轉換函數 F 的不動點

找到了轉換函數F後,下一步就是肯定其不動點了,而這個不動點就是咱們最終想要的FACT

FACT = (F (F (F ...... (F FACT) ...... )))

假設咱們已經知道了FACT非遞歸版本了,記爲g,那麼

E0 = (F g)      這時(E0 0) 對應 (FACT 0)得值,這時用不到 g
E1 = (F E0)     這時(E1 0)、(E1 1)分別對應(FACT 0)、(FACT 1)的值
E2 = (F E1)     這時(E2 0)、(E2 1)、(E2 2)分別對應(FACT 0)、(FACT 1)、(FACT 2)的值
.....
En = (F En-1)   這時....(En n)分別對應.... (FACT n)的值

能夠看到,咱們在求出(FACT n)時徹底沒有用到初始的g,換句話說就是g的取值不影響咱們計算(FACT n)
那麼咱們徹底能夠這麼定義FACT

FACT = (F (F (F ...... (F 1) ...... )))

惋惜,咱們不能這麼寫,咱們必須想個辦法表示無窮。在函數式編程中,最簡單的無窮循環是:

; Scheme
((lambda (x) (x x))
  (lambda (x) (x x)))

; JS
(function (x) {
    return x(x);
})(function(x) {
    return x(x);
});

基於此,咱們就獲得函數式編程中一重要概念 Y 算子,關於 Y 算子的嚴格推導,能夠在參考這篇文章 The Y combinator (Slight Return),這裏直接給出:

; Scheme
(define Y
  (lambda (f)
    ((lambda (x) (f (x x))
      (lambda (x) (f (x x)))))))

(define FACT (Y F))

; JS
var Y = function(f) {
    return (function(x) {
        return f(x(x));
    })(function(x) {
        return f(x(x));
    });
}
var FACT = Y(F);

這樣咱們就獲得的FACT了,但這裏獲得的FACT並不能在SchemeJS解釋器中運行,由於就像上面說的,這實際上是個死循環,若是你把上面代碼拷貝到解釋器中運行,通常能夠獲得下面的錯:

RangeError: Maximum call stack size exceeded

正則序 vs. 應用序

爲了獲得可以在SchemeJS解釋器中能夠運行的代碼,這裏須要解釋複合函數在調用時傳入參數的兩種求值策略:

  • 正則序(Normal Order),徹底展開然後歸約求值。惰性求值的語言採用這種順序。

  • 應用序(Applicative Order),先對參數求值然後應用。咱們經常使用的大部分語言都採用應用序。

舉個簡單的例子:

var p = function() {
    return (p);
}
var test = function(x, y) {
    if(x == 0) {
        return 0;
    } else {
        return y;
    }
}
test(0, (p));

上面這個例子,採用應用序的語言會產生死循環;而採用正則序的語言能夠正常返回0,由於test的第二個參數只有在x不等於0時纔會去求值。

咱們上面給出的var FACT = Y(F)在正則序的語言中是可行的,由於Y(F)中的返回值只有在真正須要時才進行求值,而在F中,n等於0時是不須要對g(n-1)進行求值的,因此這時Y(F)(5)就可以正常返回120了。

若是你以爲上面這段話很繞,一時不能理解,這樣很正常,我也是花了好久才弄明白,你能夠多找些惰性求值的文章看看。

爲了可以得出在應用序語言可用的FACT,咱們須要對上面的Y作進一步處理。思路也很簡單,爲了避免當即求值表達式,咱們能夠在其外部包一層函數,假設這裏有個表達式p

var lazy_p = function() { return p; }

這時若是想獲得p的值,就須要(lazy_p)才能夠獲得了。基於這個原理,下面給出最終版本的Y 算子

; Scheme
(define Y
  (lambda (f)
    ((lambda (x) (x x))
     (lambda (x) (f (lambda (y) ((x x) y)))))))

(define FACT (Y F))
(FACT 5)   ;===> 120

; JS
 var Y = function(f) {
     return function(x) {
         return x(x)
     }(function (x) {
         return f(function(y) {
             return x(x)(y)
         })
     })
 }
 var FACT = Y(F)
 FACT(5)   ;===> 120

好了,到如今爲止,咱們已經獲得了能夠在SchemeJS解釋器中運行FACT了,能夠看到,這裏面沒有使用函數名也實現了遞歸方式求階乘。
本文一開始給出的FACT版本在解釋器內部也會轉換爲這種形式,這也就解釋了本文所提出的問題。

總結

本文大部份內容由 SICP 4.1 小節延伸而來,寫的相對比較粗糙,不少點都沒有展開講的緣由是我本身也還沒理解透徹,爲了避免誤導你們,因此這裏就省略了(後面理解的更深入後再來填坑?)。但願感興趣的讀者可以本身去搜索相應知識點,相信確定會受益不淺。

最後,但願這篇文章對你們理解編程語言有一些幫助。有什麼不對的地方請留言指出。

相關文章
相關標籤/搜索