泛函編程(11)-延后計算-lazy evaluation


     延后計算(lazy evaluation)是指將一個表達式的值計算向后拖延直到這個表達式真正被使用的時候。在討論lazy-evaluation之前,先對泛函編程中比較特別的一個語言屬性”計算時機“(strict-ness)做些介紹。strict-ness是指系統對一個表達式計算值的時間點模式:即時計算的(strict),或者延后計算的(non-strict or lazy)。non-strict或者lazy的意思是在使用一個表達式時才對它進行計值。用個簡單直觀的例子說明吧:

1   def lazyFun(x: Int): Int = { 2       println("inside function") 3       x + 1
4   }                                               //> lazyFun: (x: Int)Int
5   lazyFun(3/0)                                    //> java.lang.ArithmeticException: / by zero

很明顯,當我們把 3/0 作為參數傳入lazyFun時,系統在進入函數前先計算這個參數的值,計算出現了異常,結果沒進入函數執行println就直接退出了。下面我們把lazyFun的參數聲明改一下變為:x: => Int:

1  def lazyFun(x: => Int): Int = { 2       println("inside function") 3       x + 1
4   }                                               //> lazyFun: (x: => Int)Int
5   lazyFun(3/0)                                    //> inside function 6                                                   //| java.lang.ArithmeticException: / by zero 7                                                   //| at ch5.stream$$anonfun$main$1$$anonfun$1.apply$mcI$sp(ch5.stream.scala:1 8                                                   //| 0)

在這個例子里我們再次向lazyFun傳入了一個Exception。系統這次進入了函數內部,我們看到println("inside function")還是運行了。這表示系統並沒有理會傳入的參數,直到表達式x + 1使用這個參數x時才計算x的值。我們看到參數x的類型是 => Int, 代表x參數是non-strict的。non-strict參數每次使用時都會重新計算一次。從內部實現機制來解釋:這是因為編譯器(compiler)遇到non-strict參數時會把一個指針放到調用堆棧里,而不是慣常的把參數的值放入。所以每次使用non-strict參數時都會重新計算一下。我們可以從下面的例子得到證實:

 

1   def pair(x: => Int):(Int, Int) = (x, x)         //> pair: (x: => Int)(Int, Int)
2   pair( {println("hello..."); 5} )                //> hello... 3                                                   //| hello... 4                                                   //| res1: (Int, Int) = (5,5)

 

以上例子里我們向pair函數傳入了一段以Int類 5 為結果的代碼作為x參數。在返回了結果(5,5)后從兩條hello...可以確認傳入的參數被計算了兩次。

實際上很多語言中的布爾表達式(Boolean Expression)都是non-strict的,包括 &&, || 。  x && y 表達式中如果x值為false的話系統不會去計算y的值,而是直接得出結果false。同樣 x || y 中如x值為true時系統不會計算y。試想想如果y需要幾千行代碼來計算的話能節省多少計算資源。

再看看以下一個if-then-else例子:

1  def if2[A](cond: Boolean, valTrue: => A, valFalse: => A): A = { 2       if (cond) { println("run valTrue..."); valTrue } 3       else { println("run valFalse..."); valFalse } 4   }                                               //> if2: [A](cond: Boolean, valTrue: => A, valFalse: => A)A
5   if2(true, 1, 0)                                 //> run valTrue... 6                                                   //| res2: Int = 1
7   if2(false, 1, 0)                                //> run valFalse... 8                                                   //| res3: Int = 0
9  

if-then-else函數if2的參數中if條件是strict的,而then和else都是non-strict的。

可以看出到底運算valTrue還是valFalse皆依賴條件cond的運算結果。但無論如何系統只會按運算一個。還是那句,如果valTrue和valFalse都是幾千行代碼的大型復雜計算,那么non-strict特性會節省大量的計算資源,提高系統運行效率。除此之外,non-strict特性是實現無限數據流(Infinite Stream)的基本要求,這部分在下節Stream里會詳細介紹。

不過從另一個方面分析:non-strict參數在函數內部有可能多次運算;如果這個函數內部多次使用了這個參數。同樣道理,如果這個參數是個大型計算的話,又會產生浪費資源的結果。在Scala語言中lazy聲明可以解決non-strict參數多次運算問題。lazy值聲明(lazy val)不但能延后賦值表達式的右邊運算,還具有緩存(cache)的作用:在真正使時才運算表達式右側,一旦賦值后不再重新計算。我們試着把上面的例子做些修改:

 

1   def pair(x: => Int):(Int, Int) = {                    //> pair: (x: => Int)(Int, Int)
2     lazy val y = x     //不運算,還沒開始使用y
3     (y,y)              //第一個y運算,第二個就使用緩存值了
4   }

這這個版本里我們使用了一個延緩值(lazy val)y。當調用這個函數時,參數的值運算在第一次使用y時會運算一次,然后存入緩存(cache),之后使用y時就無需重復計算,直接使用緩存值(cached value)。可以看看函數的調用結果:

1   pair( { println("hello..."); 5} )               //> hello... 2                                                   //| res1: (Int, Int) = (5,5)

同樣產生了重復值(5,5),但參數值運算只進行了一次,因為只有一行hello...

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM