匿名函數遞迴又一章

 Tue, 11 Nov 2008 18:00:15 +0800

很久以前寫過關於匿名函數遞迴的方法,用的是arguments屬性。不過後來發現其實不用那麼麻煩。還有一個簡單的方法,以及應用函數語言技巧的方法,可以拿來應用在匿名函數遞迴上。

javascript的function expression其實還有一個作法,讓你可以命名匿名函數,這樣在做匿名函數遞迴時就可以不用到arguments。

var a = (function f(x) {
    if (x>1) {
        return x * f(x-1);
    } else {
        return 1;
    }
})(5);
alert(a);

上面的function expression中,把function命名為f,但是他只作用在()裡面,不會對外面的scope產生影響,所以如果在alert(a)之後呼叫f會出現函數未定義的錯誤。


這幾天回頭看了一下Lambda Calculus的資料,裡面提到第一印象中Lambda Calculus似乎無法做出遞迴結構,但其實可以利用Y Combinator來做。這個作法很有趣。

按照定義,Y combinator:
Y = λ g. (λ x. g (x x)) (λ x. g (x x))

這樣的結構改成javascript就會變成:

var Y = function(g) {
    return (function(x) {
        return g(function() {
            return x(x);
        });
    })(function(x) {
        return g(function() {
            return x(x);
        });
    });
};

如果我想傳一個參數給函數,那需要調整一下這個Y combinator,讓他接受參數:

var Y = function(g) {
    return (function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    })(function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    });
};

這樣傳需要遞迴的函數給Y時,會返回一個可以接受一個參數的函數。

做階乘的函數,可以抽象為:

var g = function(f,n){
    if(n==0) {
        return 1;
    } else {
        return n*f(f,n-1);
    }
};
alert(g(g,5));

使用時,還必須把g傳給自己,才能做出遞迴。這樣做就無法匿名,否則無法傳遞g給自己做參數了。

先把把上面過程curry化備用:

var g = function(f){
    return function(n){
        if (n==0) {
            return 1;
        } else {
            return n*f(f)(n-1);
        }
    };
};
alert(g(g)(5));

使用Y combinator,他會為傳入的函數做出遞迴結構,先不用匿名函數做,比較清楚:

var g = function(f){
    return function(n){
        if (n==0) {
            return 1;
        } else {
            return n*f(n-1);
        }
    };
};
var Y = function(g) {
    return (function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    })(function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    });
};
alert(Y(g)(5));

透過Y,可以在每次遞迴中把g函數裡面的f展開為f(f),並且把g函數傳遞給自己,最後傳回一個遞迴的函數。接著傳遞參數n給這個產生的函數,就可以計算出遞迴的結果。

再來用匿名函數的方法做(跟前面等義,只是把g改成匿名函數):

var Y = function(g) {
    return (function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    })(function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    });
};
alert(Y(function(f){
    return function(n){
        if (n==0) {
            return 1;
        } else {
            return n*f(n-1);
        }
    };
})(5));

有趣吧?

使用上,可以用Y combinator產生遞迴函數來用:

var Y = function(g) {
    return (function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    })(function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    });
};
var fact = Y(function(f){
    return function(n){
        if (n==0) {
            return 1;
        } else {
            return n*f(n-1);
        }
    };
});
alert(fact(5));

接著試試看費氏數列,不使用Y combinator的狀況,必須這樣寫:

var f = function(f,n) {
    if(n==0) {
        return 0;
    } else if(n==1) {
        return 1;
    } else if(n>1) {
        return f(f,n-1) + f(f,n-2);
    }
};
alert(f(f,5));

現在可以用Y combinator來產生費氏數列函數:

var fab = Y(function(f){
    return function(n){
        if(n==0) {
            return 0;
        } else if(n==1) {
            return 1;
        } else if(n>1) {
            return f(n-1) + f(n-2);
        }
    };
});
alert(fab(5));


已經把自己弄糊塗了,稍微整理一下關於Y的思考:

var f = function(n) {
    if(n==0) {
        return 1;
    } else {
        return n * f(n-1);
    }
};
alert(f(5));
var g = function(f, n) {
    if(n==0) {
        return 1;
    } else {
        return n * f(n-1);
    }
};
alert(g(f,5));
g = function(f) {
    return function(n) {
        if(n==0) {
            return 1;
        } else {
            return n * f(n-1);
        }
    };
};
alert(g(f)(5));
var fact = g(f);
alert(fact(5));
var Y = function (g) {return g(f);}

用有名字的函數做都不是問題,使用Y combinator的關鍵在於匿名也能做出這樣的結構。第一步做出階乘的函數f。然後調整一下結構,做出一個函數g,接受f作為參數,這樣是為了匿名函數無法呼叫自己,所以把他移出去作成一個參數。接著把g做curry化。這樣做我們會發現,g(f)實際上會返回遞迴的階乘函數f。把這個過程展開,可以得出下面的結構:

var b = (function (x){
    return function(n) {
        if(n==0) {
            return 1;
        } else {
            return n * x(x)(n-1);
        }
    };
})(function (x){
    return function(n) {
        if(n==0) {
            return 1;
        } else {
            return n * x(x)(n-1);
        }
    };
});
alert(b(5));

g與f的差別,在於f靠呼叫f來達成遞迴,g靠傳入f並讓f遞迴,但是g與f的結構一樣,其實能完成的計算也一樣。所以可以靠不斷傳入g做為g的參數而返回計算階乘的函數來構成遞迴。這個過程可以靠一個匿名函數的結構達成,遞迴的地方,則由f改成g(g),產生的函數可以接收n來做階乘計算,在這個函數中又用g(g)來遞迴。我們把匿名函數上半部看成g,下半部傳入的函數叫做g1的話,執行第一次遞迴迭代的是g,之後則是g1。依序為g(g1)->g1(g1)->g1(g1)->g1(g1)......最後就可以計算出階乘的答案。關鍵其實就在於傳入的函數參數,返回的計算函數,計算函數中的遞迴結構把上述過程再度重現。

最後來看Y Combinator怎麼運作:

var Y = function(g) {
    return (function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    })(function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    });
};

其實是一樣的邏輯結構。只要稍微調整過g,把他傳給Y,產生的就是g的遞迴版本函數。關鍵還是在於x(x)(n)。另外,可以看出三個等價的函數f、g(f)、Y(g)。差別在於,前兩個不是匿名函數,最後一個傳入的參數可以是匿名函數。如果不想靠Y Combinator,也可以把這樣的結構展開,用b這個函數的方法來達成。


2008-11-12 18:12 補充:

這篇的靈感其實來自於wikipedia上關於Lambda Calculus的說明:
http://en.wikipedia.org/wiki/Lambda_calculus

另外也用到currying:
http://en.wikipedia.org/wiki/Currying

這兩篇在wikipedia上有中文版本,內容也相當完整。

其實寫這篇的時候,雖然做出結果,但是覺得沒有把他考慮清楚,就重新想了一遍,所以內容前後有一些地方有重複甚至有些矛盾的。不過還是把他當作一個紀錄啦。


2008-11-12 23:02

阿,還有一些參考資料是關於continuation passing style:
http://en.wikipedia.org/wiki/Continuation_passing_style

總之,這篇其實講functional language大於平常的javascript programming。既然Crockford在他的書中提到:「JavaScript是第一個躍上主流的lambda語言。」,有機會還是會想嘗試看看他屬於這一部份的特性。