惰性求值

2018-02-24 15:53 更新

惰性求值

惰性求值(或是延遲求值)是一種有趣的技術(shù),而當(dāng)我們投入函數(shù)式編程的懷抱后這種技術(shù)就有了得以實現(xiàn)的可能。前面介紹并發(fā)執(zhí)行的時候已經(jīng)提到過如下代碼:

String s1 = somewhatLongOperation1();
String s2 = somewhatLongOperation2();
String s3 = concatenate(s1, s2);

在指令式語言中以上代碼執(zhí)行的順序是顯而易見的。由于每個函數(shù)都有可能改動或者依賴于其外部的狀態(tài),因此必須順序執(zhí)行。先是計算somewhatLongOperation1,然后到somewhatLongOperation2,最后執(zhí)行concatenate。函數(shù)式語言就不一樣了。
在前面討論過,somewhatLongOperation1和somewhatLongOperation2是可以并發(fā)執(zhí)行的,因為函數(shù)式語言保證了一點:沒有函數(shù)會影響或者依賴于全局狀態(tài)。可是萬一我們不想要這兩個函數(shù)并發(fā)執(zhí)行呢?這種情況下是不是也還是要順序執(zhí)行這些函數(shù)?答案是否定的。只有到了執(zhí)行需要s1、s2作為參數(shù)的函數(shù)的時候,才真正需要執(zhí)行這兩個函數(shù)。于是在concatenate這個函數(shù)沒有執(zhí)行之前,都沒有需要去執(zhí)行這兩個函數(shù):這些函數(shù)的執(zhí)行可以一直推遲到concatenate()中需要用到s1和s2的時候。假如把concatenate換成另外一個函數(shù),這個函數(shù)中有條件判斷語句而且實際上只會需要兩個參數(shù)中的其中一個,那么就完全沒有必要執(zhí)行計算另外一個參數(shù)的函數(shù)了!Haskell語言就是一個支持惰性求值的例子。Haskell不能保證任何語句會順序執(zhí)行(甚至完全不會執(zhí)行到),因為Haskell的代碼只有在需要的時候才會被執(zhí)行到。
除了這些優(yōu)點,惰性求值也有缺點。這里介紹了它的優(yōu)點,我們將在下一章節(jié)介紹這些缺點以及如何克服它們。

代碼優(yōu)化

惰性求值使得代碼具備了巨大的優(yōu)化潛能。支持惰性求值的編譯器會像數(shù)學(xué)家看待代數(shù)表達(dá)式那樣看待函數(shù)式程序:抵消相同項從而避免執(zhí)行無謂的代碼,安排代碼執(zhí)行順序從而實現(xiàn)更高的執(zhí)行效率甚至是減少錯誤。在此基礎(chǔ)上優(yōu)化是不會破壞代碼正常運行的。嚴(yán)格使用形式系統(tǒng)的基本元素進(jìn)行編程帶來的最大的好處,是可以用數(shù)學(xué)方法分析處理代碼,因為這樣的程序是完全符合數(shù)學(xué)法則的。

抽象化控制結(jié)構(gòu)

惰性求值技術(shù)提供了更高階的抽象能力,這提供了實現(xiàn)程序設(shè)計獨特的方法。比如說下面的控制結(jié)構(gòu):

unless(stock.isEuropean()) {
    sendToSEC(stock);
}

程序中只有在stock為European的時候才執(zhí)行sendToSEC。如何實現(xiàn)例子中的unless?如果沒有惰性求值就需要求助于某種形式的宏(譯者:用if不行么?),不過在像Haskell這樣的語言中就不需要那么麻煩了。直接實現(xiàn)一個unless函數(shù)就可以!

void unless(boolean condition, List code) {
    if(!condition)
        code;
}

請注意,如果condition值為真,那就不會計算code。在其他嚴(yán)格語言(見嚴(yán)格求值)中這種行為是做不到的,因為在進(jìn)入unless這個函數(shù)之前,作為參數(shù)的code已經(jīng)被計算過了。

無窮數(shù)據(jù)結(jié)構(gòu)

惰性求值技術(shù)允許定義無窮數(shù)據(jù)結(jié)構(gòu),這要在嚴(yán)格語言中實現(xiàn)將非常復(fù)雜。例如一個儲存Fibonacci數(shù)列數(shù)字的列表。很明顯這樣一個列表是無法在有限的時間內(nèi)計算出這個無窮的數(shù)列并存儲在內(nèi)存中的。在像Java這樣的嚴(yán)格語言中,可以定義一個Fibonacci函數(shù),返回這個序列中的某個數(shù)。而在Haskell或是類似的語言中,可以把這個函數(shù)進(jìn)一步抽象化并定義一個Fibonacci數(shù)列的無窮列表結(jié)構(gòu)。由于語言本身支持惰性求值,這個列表中只有真正會被用到的數(shù)才會被計算出來。這讓我們可以把很多問題抽象化,然后在更高的層面上解決它們(比如可以在一個列表處理函數(shù)中處理無窮多數(shù)據(jù)的列表)。

不足之處

俗話說天下沒有免費的午餐?。惰性求值當(dāng)然也有其缺點。其中最大的一個就是,嗯,惰性。現(xiàn)實世界中很多問題還是需要嚴(yán)格求值的。比如說下面的例子:

System.out.println("Please enter your name: ");
System.in.readLine();

在惰性語言中沒人能保證第一行會中第二行之前執(zhí)行!這也就意味著我們不能處理IO,不能調(diào)用系統(tǒng)函數(shù)做任何有用的事情(這些函數(shù)需要按照順序執(zhí)行,因為它們依賴于外部狀態(tài)),也就是說不能和外界交互了!如果在代碼中引入支持順序執(zhí)行的代碼原語,那么我們就失去了用數(shù)學(xué)方式分析處理代碼的優(yōu)勢(而這也意味著失去了函數(shù)式編程的所有優(yōu)勢)。幸運的是我們還不算一無所有。數(shù)學(xué)家們研究了不同的方法用以保證代碼按一定的順序執(zhí)行(in a functional setting?)。這一來我們就可以同時利用到函數(shù)式和指令式編程的優(yōu)點了!這些方法有continuations,monads以及uniqueness typing。這篇文章僅僅介紹了continuations,以后再討論monads和uniqueness typing。有意思的是呢,coutinuations處理強(qiáng)制代碼以特定順序執(zhí)行之外還有其他很多出處,這些我們在后面也會提及。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號