惰性求值(或是延遲求值)是一種有趣的技術(shù),而當(dāng)我們投入函數(shù)式編程的懷抱后這種技術(shù)就有了得以實(shí)現(xiàn)的可能。前面介紹并發(fā)執(zhí)行的時(shí)候已經(jīng)提到過(guò)如下代碼:
String s1 = somewhatLongOperation1();
String s2 = somewhatLongOperation2();
String s3 = concatenate(s1, s2);
在指令式語(yǔ)言中以上代碼執(zhí)行的順序是顯而易見(jiàn)的。由于每個(gè)函數(shù)都有可能改動(dòng)或者依賴于其外部的狀態(tài),因此必須順序執(zhí)行。先是計(jì)算somewhatLongOperation1,然后到somewhatLongOperation2,最后執(zhí)行concatenate。函數(shù)式語(yǔ)言就不一樣了。
在前面討論過(guò),somewhatLongOperation1和somewhatLongOperation2是可以并發(fā)執(zhí)行的,因?yàn)楹瘮?shù)式語(yǔ)言保證了一點(diǎn):沒(méi)有函數(shù)會(huì)影響或者依賴于全局狀態(tài)??墒侨f(wàn)一我們不想要這兩個(gè)函數(shù)并發(fā)執(zhí)行呢?這種情況下是不是也還是要順序執(zhí)行這些函數(shù)?答案是否定的。只有到了執(zhí)行需要s1、s2作為參數(shù)的函數(shù)的時(shí)候,才真正需要執(zhí)行這兩個(gè)函數(shù)。于是在concatenate這個(gè)函數(shù)沒(méi)有執(zhí)行之前,都沒(méi)有需要去執(zhí)行這兩個(gè)函數(shù):這些函數(shù)的執(zhí)行可以一直推遲到concatenate()中需要用到s1和s2的時(shí)候。假如把concatenate換成另外一個(gè)函數(shù),這個(gè)函數(shù)中有條件判斷語(yǔ)句而且實(shí)際上只會(huì)需要兩個(gè)參數(shù)中的其中一個(gè),那么就完全沒(méi)有必要執(zhí)行計(jì)算另外一個(gè)參數(shù)的函數(shù)了!Haskell語(yǔ)言就是一個(gè)支持惰性求值的例子。Haskell不能保證任何語(yǔ)句會(huì)順序執(zhí)行(甚至完全不會(huì)執(zhí)行到),因?yàn)镠askell的代碼只有在需要的時(shí)候才會(huì)被執(zhí)行到。
除了這些優(yōu)點(diǎn),惰性求值也有缺點(diǎn)。這里介紹了它的優(yōu)點(diǎn),我們將在下一章節(jié)介紹這些缺點(diǎn)以及如何克服它們。
惰性求值使得代碼具備了巨大的優(yōu)化潛能。支持惰性求值的編譯器會(huì)像數(shù)學(xué)家看待代數(shù)表達(dá)式那樣看待函數(shù)式程序:抵消相同項(xiàng)從而避免執(zhí)行無(wú)謂的代碼,安排代碼執(zhí)行順序從而實(shí)現(xiàn)更高的執(zhí)行效率甚至是減少錯(cuò)誤。在此基礎(chǔ)上優(yōu)化是不會(huì)破壞代碼正常運(yùn)行的。嚴(yán)格使用形式系統(tǒng)的基本元素進(jìn)行編程帶來(lái)的最大的好處,是可以用數(shù)學(xué)方法分析處理代碼,因?yàn)檫@樣的程序是完全符合數(shù)學(xué)法則的。
惰性求值技術(shù)提供了更高階的抽象能力,這提供了實(shí)現(xiàn)程序設(shè)計(jì)獨(dú)特的方法。比如說(shuō)下面的控制結(jié)構(gòu):
unless(stock.isEuropean()) {
sendToSEC(stock);
}
程序中只有在stock為European的時(shí)候才執(zhí)行sendToSEC。如何實(shí)現(xiàn)例子中的unless?如果沒(méi)有惰性求值就需要求助于某種形式的宏(譯者:用if不行么?),不過(guò)在像Haskell這樣的語(yǔ)言中就不需要那么麻煩了。直接實(shí)現(xiàn)一個(gè)unless函數(shù)就可以!
void unless(boolean condition, List code) {
if(!condition)
code;
}
請(qǐng)注意,如果condition值為真,那就不會(huì)計(jì)算code。在其他嚴(yán)格語(yǔ)言(見(jiàn)嚴(yán)格求值)中這種行為是做不到的,因?yàn)樵谶M(jìn)入unless這個(gè)函數(shù)之前,作為參數(shù)的code已經(jīng)被計(jì)算過(guò)了。
惰性求值技術(shù)允許定義無(wú)窮數(shù)據(jù)結(jié)構(gòu),這要在嚴(yán)格語(yǔ)言中實(shí)現(xiàn)將非常復(fù)雜。例如一個(gè)儲(chǔ)存Fibonacci數(shù)列數(shù)字的列表。很明顯這樣一個(gè)列表是無(wú)法在有限的時(shí)間內(nèi)計(jì)算出這個(gè)無(wú)窮的數(shù)列并存儲(chǔ)在內(nèi)存中的。在像Java這樣的嚴(yán)格語(yǔ)言中,可以定義一個(gè)Fibonacci函數(shù),返回這個(gè)序列中的某個(gè)數(shù)。而在Haskell或是類似的語(yǔ)言中,可以把這個(gè)函數(shù)進(jìn)一步抽象化并定義一個(gè)Fibonacci數(shù)列的無(wú)窮列表結(jié)構(gòu)。由于語(yǔ)言本身支持惰性求值,這個(gè)列表中只有真正會(huì)被用到的數(shù)才會(huì)被計(jì)算出來(lái)。這讓我們可以把很多問(wèn)題抽象化,然后在更高的層面上解決它們(比如可以在一個(gè)列表處理函數(shù)中處理無(wú)窮多數(shù)據(jù)的列表)。
俗話說(shuō)天下沒(méi)有免費(fèi)的午餐?。惰性求值當(dāng)然也有其缺點(diǎn)。其中最大的一個(gè)就是,嗯,惰性?,F(xiàn)實(shí)世界中很多問(wèn)題還是需要嚴(yán)格求值的。比如說(shuō)下面的例子:
System.out.println("Please enter your name: ");
System.in.readLine();
在惰性語(yǔ)言中沒(méi)人能保證第一行會(huì)中第二行之前執(zhí)行!這也就意味著我們不能處理IO,不能調(diào)用系統(tǒng)函數(shù)做任何有用的事情(這些函數(shù)需要按照順序執(zhí)行,因?yàn)樗鼈円蕾囉谕獠繝顟B(tài)),也就是說(shuō)不能和外界交互了!如果在代碼中引入支持順序執(zhí)行的代碼原語(yǔ),那么我們就失去了用數(shù)學(xué)方式分析處理代碼的優(yōu)勢(shì)(而這也意味著失去了函數(shù)式編程的所有優(yōu)勢(shì))。幸運(yùn)的是我們還不算一無(wú)所有。數(shù)學(xué)家們研究了不同的方法用以保證代碼按一定的順序執(zhí)行(in a functional setting?)。這一來(lái)我們就可以同時(shí)利用到函數(shù)式和指令式編程的優(yōu)點(diǎn)了!這些方法有continuations,monads以及uniqueness typing。這篇文章僅僅介紹了continuations,以后再討論monads和uniqueness typing。有意思的是呢,coutinuations處理強(qiáng)制代碼以特定順序執(zhí)行之外還有其他很多出處,這些我們?cè)诤竺嬉矔?huì)提及。
更多建議: