按需调用的简单示例

Simple example of call-by-need

我正在尝试理解背后的定理 "call-by-need." 我确实理解定义,但我有点困惑。我想看一个简单的例子,它展示了按需调用是如何工作的。

在阅读了之前的一些帖子后,我发现 Haskell 使用了这种评估。是否有任何其他编程语言支持此功能?

我了解了 Scala 的按名称调用,并且我确实理解按名称调用和按需要调用是相似的,但不同之处在于按需要调用将保持评估价值。但我真的很想看到一个真实的例子(它不必在 Haskell 中),它显示了按需调用。

函数

say_hello numbers = putStrLn "Hello!"

忽略其 numbers 参数。在 call-by-value 语义下,即使参数被忽略,函数调用站点的参数也可能需要计算,这可能是因为程序其余部分所依赖的副作用。

在Haskell中,我们可以称say_hello

say_hello [1..]

其中 [1..] 是自然数的无限列表。在按值调用语义下,CPU 将 运行 停止尝试构建无限列表并且永远不会到达 say_hello

Haskell 仅仅输出

$ runghc cbn.hs
Hello!

对于不那么引人注目的例子,前十个自然数是

ghci> take 10 [1..]
[1,2,3,4,5,6,7,8,9,10]

前十个赔率是

ghci> take 10 $ filter odd [1..]
[1,3,5,7,9,11,13,15,17,19]

call-by-need 语义下,每个值——即使是上面示例中概念上无限的值——仅在需要的范围内进行评估,不会超过。

更新:一个简单的例子,要求:

ff 0 = 1
ff 1 = 1
ff n = go (ff (n-1))
  where
  go x = x + x

在按名称调用下,go 的每次调用都会对 ff (n-1) 求值两次,每次针对其定义中 x 的每次出现(因为 + 是严格的在两个参数中,即要求它们两个的值)。

在按需调用下,go的参数最多被评估一次。具体来说,这里 x 的值只被找出一次,并在表达式 x + x 中第二次出现 x 时重复使用。如果不需要,则 x 根本不会被计算,就像按名称调用一样。

在按值调用下,go 的参数总是在进入函数体之前只计算一次,即使它没有在函数体的任何地方使用。


这是我在 Haskell 的背景下对它的理解。

According to Wikipedia, "call by need is a memoized variant of call by name where, if the function argument is evaluated, that value is stored for subsequent uses."

按姓名呼叫:

take 10 . filter even $ [1..]

对于一个消费者来说,产生的价值在产生之后就消失了,所以它还不如叫名字。

按需致电:

import qualified Data.List.Ordered as O

h = 1 : map (2*) h <> map (3*) h <> map (5*) h
    where
    (<>) = O.union

不同之处在于,这里的 h 列表被多个消费者以不同的节奏重复使用,因此必须记住生成的值。在按名称调用的语言中,这里会有很多计算工作的复制,因为 h 的计算表达式将在其每次出现时被替换,从而导致对每个单独的计算。在按需调用--capable 语言中,如 Haskell 计算 h 元素的结果在对 h 的每个引用之间共享.

另一个例子是,大多数由fix定义的数据只能在按需调用的情况下使用。对于按值调用,我们最多只能使用 Y 组合器。

参见: and its linked entries and comments (among them, this, and its links, like Can fold be used to create infinite lists?)。