簡體   English   中英

Scala中的按名稱致電與Haskell中的惰性評估?

[英]Call-by-name in Scala vs lazy evaluation in Haskell?

Haskell的惰性評估永遠不會比渴望的評估采取更多的評估步驟。

另一方面,Scala的按名稱進行的評估可能比按值進行的評估需要更多的評估步驟(如果短路收益遠大於重復計算的成本所抵消)。

我認為按名稱致電大致相當於懶惰的評估。 為什么在時間保證上有如此大的差異?

我猜想也許Haskell語言指定在評估過程中必須使用記憶。 但是在那種情況下,Scala為什么不這樣做呢?

評估策略的名稱有一定的廣度,但大致可以歸結為:

  • 按名稱調用參數幾乎是以調用函數時所用的任何形式(未經評估)替換為函數主體。 這意味着可能需要在體內對其進行多次評估。

    在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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM