[英]Scala Stream call-by-need (lazy) vs call-by-name
[英]Call-by-name in Scala vs lazy evaluation in Haskell?
評估策略的名稱有一定的廣度,但大致可以歸結為:
按名稱調用參數幾乎是以調用函數時所用的任何形式(未經評估)替換為函數主體。 這意味着可能需要在體內對其進行多次評估。
在Scala中,您將其編寫為:
scala> def f(x:=> Int): Int = x + x scala> f({ println("evaluated"); 1 }) evaluated evaluated 2
在Haskell中,您沒有內置的方法可以執行此操作,但是始終可以將按名稱調用的值表示為() -> a
類型的函數。 但是,由於參照透明性,這有點模糊-您將無法像使用Scala那樣進行測試(並且編譯器可能會優化調用的“按名稱”部分)。
按需調用 (懶惰...之類的),在調用函數時不會評估參數,但是在第一次時是必需的。 那時,它也被緩存。 之后,每當再次需要該參數時,都會查詢緩存的值。
在Scala中,您不會將函數參數聲明為惰性的,而是將聲明設為惰性的:
scala> lazy x: Int = { println("evaluated"); 1 } scala> x + x evaluated 2
在Haskell中,這就是默認情況下所有功能的工作方式。
按值調用 (渴望,幾乎每種語言都執行),在調用函數時會評估參數,即使函數最終沒有使用這些參數也是如此。
在Scala中,這是默認情況下函數的工作方式。
scala> def f(x: Int): Int = x + x scala> f({ println("evaluated"); 1 }) evaluated 2
在Haskell中,您可以在函數參數上使用bang模式強制執行此行為:
ghci> :{ ghci> f :: Int -> Int ghci> f !x = x ghci> :}
除非您具有參照透明性,否則很難推斷出惰性評估,因為這樣一來,您就需要准確計算出何時評估您的惰性值。 由於Scala是為與Java互操作而構建的,因此它需要支持命令式,副作用較大的編程。 因此,在許多情況下,在Scala中使用lazy
不是一個好主意。
此外, lazy
會帶來性能開銷:您需要進行額外的間接檢查,以檢查該值是否已被評估。 在Scala中,這轉化為一堆更多的對象,這給垃圾收集器帶來了更大的壓力。
最后,在有些情況下,延遲評估會留下“空間”泄漏。 例如,在Haskell中,通過將它們加在一起從右邊折疊一個大的數字列表是一個壞主意,因為Haskell會在評估它們之前建立對(+)
一系列懶惰調用(+)
實際上,您只需要即使在簡單的情況下,您也會遇到一個空間問題的著名例子,例如foldr
vs foldl
vs foldl'
。
我不知道為什么斯卡拉 沒有 事實證明,它可以 “適當地”進行惰性評估-可能實施起來並不那么簡單,尤其是當您希望該語言與JVM順利交互時。
如您所見,按名稱調用不等同於惰性求值,而是將類型a
的參數替換為類型() -> a
。 這樣的功能包含作為一個普通的相同的信息量a
值(類型是同構的),但在那個值實際上讓你總是需要的功能,適用於()
偽參數。 對函數進行兩次評估時,將獲得兩次相同的結果 ,但是每次都必須重新計算一次(因為自動記憶函數是不可行的 )。
惰性計算相當於替換類型的參數a
與一種類型的一個參數,成為如下的OO類:
class Lazy<A> {
function<A()> computer;
option<A> containedValue;
public:
Lazy(function<A()> computer):
computer = computer
, containerValue = Nothing
{}
A operator()() {
if isNothing(containedValue) {
containedValue = Just(computer());
}
return fromJust(containedValue);
}
}
本質上,這只是圍繞特定的“按名稱調用”功能類型的備注包裝。 什么是不太好的是,這種包裝在副作用的根本途徑依賴:當懶惰值先進行計算,則必須將變異containedValue
代表事實值是目前已知的。 Haskell的這種機制在其運行時的心臟處扎根,並且經過了線程安全性等方面的良好測試。但是,在一種嘗試盡可能公開地使用命令式VM的語言中,如果這些虛假的突變可能會引起巨大的麻煩。與明顯的副作用交織在一起。 尤其是,因為真正有趣的懶惰應用程序不僅具有單個函數參數lazy(不會為您帶來很多好處),而且還可以將惰性值分散到整個深度數據結構的脊柱中。 最后,不僅是延遲函數要比進入惰性函數更晚進行評估,而且隨着惰性數據結構的消耗,它是對此類函數的嵌套調用的全部種子 (實際上,可能無限多個!)。
因此,Scala通過默認情況下不使任何內容成為惰性來避免這種危險,盡管正如Alec所說的那樣,它確實提供了一個lazy
關鍵字,該關鍵字基本上在值中添加了類似上面的記憶功能包裝。
這可能很有用,但實際上並不適合評論。
您可以在Scala中編寫一個函數,使其行為類似於Haskell的按需調用(對參數),方法是按名稱進行參數調用,並在函數開始時進行惰性計算:
def foo(x: => Int) = {
lazy val _x = x
// make sure you only use _x below, not x
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.