摘要:如果存在這么個函數(shù),那么我們就可以通過求解的不動點來求出了。尋找轉(zhuǎn)換函數(shù)的不動點找到了轉(zhuǎn)換函數(shù)后,下一步就是確定其不動點了,而這個不動點就是我們最終想要的。
本文原發(fā)于個人博客
遞歸 作為計算機科學(xué)中很重要的一個概念,應(yīng)用范圍非常廣泛。比較重要的數(shù)據(jù)結(jié)構(gòu),像樹、圖,本身就是遞歸定義的。
比較常見的遞歸算法有階乘、斐波那契數(shù)等,它們都是在定義函數(shù)的同時又引用本身,對于初學(xué)者來說也比較好理解,但是如果你對編程語言,特別是函數(shù)式語言,有所研究,可能就會有下面的疑問:
一個函數(shù)在還沒有定義完整時,為什么能夠直接調(diào)用的呢?
這篇文章主要是解答上面這個問題。閱讀下面的內(nèi)容,你需要有些函數(shù)式編程的經(jīng)驗,為了保證你能夠比較愉快的閱讀本文,你至少能看懂前綴表達式。相信讀完本文后,你將會對編程語言有一全新的認識。
本文所有演示代碼有Scheme、JS兩個版本。
下面的講解以階乘為例子:
; 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 這個函數(shù)為什么在沒有被定義完整時,就可以調(diào)用了。
解決一個新問題,常見的做法就是類比之前解決的問題。我們要解決的這個問題和求解下面的等式很類似:
2x = x + 1
在等號兩邊都出現(xiàn)了x,要想解決這個問題,最簡單的方式就是將等號右邊的x移到左邊即可。這樣就知道x是什么值了。
但是我們的問題比這個要復(fù)雜些了,因為我們這里需要用if、n、*、-這四個符號來表示FACT,可以這么類比是因為一個程序無非就是通過一些具有特定語意的符號(編程語言規(guī)定)構(gòu)成的。
再進一步思考,FACT 需要用四個符號來表示,這和我們求解多元方程組的解不是很像嘛:
x + y = 3 x - y = 1
為了求解上面方程組,一般可以轉(zhuǎn)為下面的形式:
x = 3 - y y = x - 1
即
(x, y) = T (x, y)
其中的T為一個轉(zhuǎn)換,在線性代數(shù)其實就是個矩陣,根據(jù)矩陣T的一些性質(zhì),我們可以判定(x ,y)是否有解,以及解的個數(shù)。
對比此,我們可以把問題轉(zhuǎn)化為下面的形式:
FACT = F (FACT)
上面的F為某種轉(zhuǎn)換,在這里其實就是個需要一個函數(shù)作為參數(shù)并且返回一個函數(shù)的函數(shù)。如果存在這么個F函數(shù),那么我們就可以通過求解F的不動點來求出FACT了。
但這里有個問題,即便我們知道了F的存在,我們也無法確定其是否存在不動點,以及如果存在,不動點的個數(shù)又是多少?
計算機科學(xué)并不像數(shù)學(xué)領(lǐng)域有那么多可以套用的定理。
尋找轉(zhuǎn)換函數(shù) F證明F是否存在是個比較難的問題,不在本文的討論范圍內(nèi),這涉及到Denotational semantics領(lǐng)域的知識,感興趣的讀者可以自己去網(wǎng)上查找相關(guān)資料。
這里直接給出FACT對應(yīng)的函數(shù)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變動不大,就是把函數(shù)內(nèi)FACT的調(diào)用換成了參數(shù)g而已,其實我們常見的遞歸算法都可以這么做。
尋找轉(zhuǎn)換函數(shù) F 的不動點找到了轉(zhuǎn)換函數(shù)F后,下一步就是確定其不動點了,而這個不動點就是我們最終想要的FACT。
FACT = (F (F (F ...... (F FACT) ...... )))
假設(shè)我們已經(jīng)知道了FACT非遞歸版本了,記為g,那么
E0 = (F g) 這時(E0 0) 對應(yīng) (FACT 0)得值,這時用不到 g E1 = (F E0) 這時(E1 0)、(E1 1)分別對應(yīng)(FACT 0)、(FACT 1)的值 E2 = (F E1) 這時(E2 0)、(E2 1)、(E2 2)分別對應(yīng)(FACT 0)、(FACT 1)、(FACT 2)的值 ..... En = (F En-1) 這時....(En n)分別對應(yīng).... (FACT n)的值
可以看到,我們在求出(FACT n)時完全沒有用到初始的g,換句話說就是g的取值不影響我們計算(FACT n)。
那么我們完全可以這么定義FACT:
FACT = (F (F (F ...... (F 1) ...... )))
可惜,我們不能這么寫,我們必須想個辦法表示無窮。在函數(shù)式編程中,最簡單的無窮循環(huán)是:
; Scheme ((lambda (x) (x x)) (lambda (x) (x x))) ; JS (function (x) { return x(x); })(function(x) { return x(x); });
基于此,我們就得到函數(shù)式編程中一重要概念 Y 算子,關(guān)于 Y 算子的嚴格推導(dǎo),可以在參考這篇文章 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并不能在Scheme或JS解釋器中運行,因為就像上面說的,這其實是個死循環(huán),如果你把上面代碼拷貝到解釋器中運行,一般可以得到下面的錯:
RangeError: Maximum call stack size exceeded正則序 vs. 應(yīng)用序
為了得到能夠在Scheme或JS解釋器中可以運行的代碼,這里需要解釋復(fù)合函數(shù)在調(diào)用時傳入?yún)?shù)的兩種求值策略:
正則序(Normal Order),完全展開而后歸約求值。惰性求值的語言采用這種順序。
應(yīng)用序(Applicative Order),先對參數(shù)求值而后應(yīng)用。我們常用的大部分語言都采用應(yīng)用序。
舉個簡單的例子:
var p = function() { return (p); } var test = function(x, y) { if(x == 0) { return 0; } else { return y; } } test(0, (p));
上面這個例子,采用應(yīng)用序的語言會產(chǎn)生死循環(huán);而采用正則序的語言可以正常返回0,因為test的第二個參數(shù)只有在x不等于0時才會去求值。
我們上面給出的var FACT = Y(F)在正則序的語言中是可行的,因為Y(F)中的返回值只有在真正需要時才進行求值,而在F中,n等于0時是不需要對g(n-1)進行求值的,所以這時Y(F)(5)就能夠正常返回120了。
如果你覺得上面這段話很繞,一時不能理解,這樣很正常,我也是花了很久才弄明白,你可以多找些惰性求值的文章看看。
為了能夠得出在應(yīng)用序語言可用的FACT,我們需要對上面的Y做進一步處理。思路也很簡單,為了不立即求值表達式,我們可以在其外部包一層函數(shù),假設(shè)這里有個表達式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
好了,到現(xiàn)在為止,我們已經(jīng)得到了可以在Scheme或JS解釋器中運行FACT了,可以看到,這里面沒有使用函數(shù)名也實現(xiàn)了遞歸方式求階乘。
本文一開始給出的FACT版本在解釋器內(nèi)部也會轉(zhuǎn)換為這種形式,這也就解釋了本文所提出的問題。
本文大部分內(nèi)容由 SICP 4.1 小節(jié)延伸而來,寫的相對比較粗糙,很多點都沒有展開講的原因是我自己也還沒理解透徹,為了不誤導(dǎo)大家,所以這里就省略了(后面理解的更深刻后再來填坑?)。希望感興趣的讀者能夠自己去搜索相應(yīng)知識點,相信肯定會受益匪淺。
最后,希望這篇文章對大家理解編程語言有一些幫助。有什么不對的地方請留言指出。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/78729.html
摘要:結(jié)論這次主要介紹了函數(shù)式編程中的函數(shù)的原理和實現(xiàn)方法,由于篇幅原因,我把打算分析的源碼實現(xiàn)放到下一篇來介紹,可以說實現(xiàn)的更加函數(shù)式,需要單獨好好分析。 上一篇文章介紹了javascript函數(shù)式編程中curry(柯里化)的實現(xiàn),當(dāng)然那個柯里化是有限參數(shù)的柯里化,等有機會在補上無限參數(shù)的那一種柯里化,這次主要說的是javascript函數(shù)式編程中另外一個很重要的函數(shù)compose,com...
摘要:在定義函數(shù)時給定的名稱稱作形參,在調(diào)用函數(shù)時你所提供給函數(shù)的值稱作實參。調(diào)用函數(shù)要調(diào)用一個函數(shù),需要知道函數(shù)的名稱和參數(shù)。默認參數(shù)值可以有效幫助解決這一情況。是默認參數(shù)定義默認參數(shù)要牢記一點默認參數(shù)必須指向不變對象。 關(guān)于數(shù)據(jù)科學(xué)在做什么,我們已經(jīng)在前兩篇文章中進行了總結(jié),即專題概述和描述性統(tǒng)計分析。要進行數(shù)據(jù)科學(xué)的探索,需要一個好工具,就是Python。從本篇開始,將總結(jié)學(xué)習(xí)Pyth...
摘要:函數(shù)內(nèi)的變量被稱為局部變量,這是與全局變量相反的概念。有一些進行函數(shù)式編程的機制。繼承以通用的類為基礎(chǔ)建立專門的類對象。 6.4.5 參數(shù)收集的逆過程 假設(shè)有如下函數(shù): def add(x,y): return x+y 比如說有個包含由兩個相加的數(shù)字組成的元組: params = (1,2) 使用*運算符對參數(shù)進行分配,不過是在調(diào)用而不是在定義時使用: >>> add(*params)...
摘要:遞歸閉包模仿塊級作用域私有變量小結(jié)在編程中,使用函數(shù)表達式可以無需對函數(shù)命名,從而實現(xiàn)動態(tài)編程。匿名函數(shù)也稱為拉姆達函數(shù)。函數(shù)聲明要求有名字,但函數(shù)表達式不需要。中的函數(shù)表達式和閉包都是極其有用的特性,利用它們可以實現(xiàn)很多功能。 1、遞歸 2、閉包 3、模仿塊級作用域 4、私有變量 5、小結(jié) 在JavaScript編程中,使用函數(shù)表達式可以無需對函數(shù)命名,從而實現(xiàn)動態(tài)編程。匿名函數(shù)也稱...
摘要:如下所示這個是不是有點作弊的嫌疑,我們再往下,把上面這個函數(shù)整成箭頭函數(shù)式的匿名函數(shù)的樣子。動用高階函數(shù)的遞歸但是上面這個遞歸的匿名函數(shù)在自己調(diào)用自己,所以,代碼中有的實參。 今天在微博上看到了 有人分享了下面的這段函數(shù)式代碼,我把代碼貼到下面,不過我對原來的代碼略有改動,對于函數(shù)式的版本,咋一看,的確令人非常費解,仔細看一下,你可能就暈掉了,似乎完全就是天書,看上去非常裝逼,哈哈。不...
閱讀 1837·2023-04-25 14:49
閱讀 3117·2021-09-30 09:47
閱讀 3101·2021-09-06 15:00
閱讀 2224·2019-08-30 13:16
閱讀 1436·2019-08-30 10:48
閱讀 2668·2019-08-29 15:11
閱讀 1287·2019-08-26 14:06
閱讀 1663·2019-08-26 13:30