Scala 中的按名称调用 vs Haskell 中的惰性求值?
Call-by-name in Scala vs lazy evaluation in Haskell?
Haskell 的懒惰求值 will never 比急切求值需要更多的求值步骤。
另一方面,Scala 的按名称调用评估 may require 评估步骤比按值调用多(如果短路收益被重复计算的成本抵消)。
我认为按名称调用大致等同于惰性求值。那么为什么在时间保证上会有这样的差异呢?
我猜测可能 Haskell 语言指定在评估期间必须使用记忆;但在那种情况下,为什么 Scala 不这样做呢?
评估策略的名称有一定的广度,但大致可以归结为:
按名称调用 参数几乎只是以调用函数时的任何(未评估)形式替换到函数体中。这意味着它可能需要在体内进行多次评估。
在 Scala 中,您可以这样写:
scala> def f(x:=> Int): Int = x + x
scala> f({ println("evaluated"); 1 })
evaluated
evaluated
2
在 Haskell 中,您没有内置的方法来执行此操作,但您始终可以将 call-by-name 值表示为类型 () -> a
的函数。不过,由于引用透明性,这有点模糊——您将无法像使用 Scala 那样测试它(并且编译器可能会优化您调用的 "by name" 部分)。
call by need(懒惰......有点)调用函数时不评估参数,但在第一次需要时.在那一刻,它也被缓存了。之后,每当再次需要该参数时,就会查找缓存的值。
在 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 互操作而构建的,因此它需要支持命令式 side-effectful 编程。因此,在许多情况下 不是 在 Scala 中使用 lazy
的好主意。
此外,lazy
有性能开销:您需要有一个额外的间接寻址来检查该值是否已经被计算过。在 Scala 中,这意味着更多的对象,这给垃圾收集器带来了更大的压力。
最后,在某些情况下,惰性求值会留下 "space" 漏洞。例如,在 Haskell 中,通过将它们相加从右边折叠一大列数字是一个坏主意,因为 Haskell 将在计算之前建立对 (+)
的这个巨大系列惰性调用他们(实际上你只需要它来有一个累加器。一个著名的 space 问题的例子是 foldr
vs foldl
vs foldl'
.
我不知道为什么 Scala 没有结果 有 “适当的”惰性求值——可能它的实现并不那么简单,尤其是当您希望语言与 JVM 顺利交互时。
Call-by-name(如您所见)不等同于惰性求值,而是用 () -> a
类型的参数替换 a
类型的参数。这样的函数包含与普通 a
值(类型是同构的)相同数量的信息,但要实际获得该值,您始终需要将函数应用于 ()
伪参数。当您对该函数求值两次时,您将得到 twice the same result, but it must each time be calculated anew (since automatically memoising functions is not feasible).
惰性求值等同于用行为类似于以下 OO class:
的类型的参数替换类型 a
的参数
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);
}
}
这本质上只是围绕特定 call-by-name– 函数类型的 memoisation-wrapper。不太好的是,这个包装器以一种基本方式依赖于 side-effects:当第一次评估惰性值时,您必须改变 containedValue
以表示该值现在已知的事实。 Haskell 在其运行时的核心有这种机制 hard-baked,well-tested 用于 thread-safety 等等。但是在一种试图尽可能公开地使用命令式 VM 的语言中,如果这些虚假突变与显式 side-effects 交织在一起,可能会引起严重的头痛。特别是,因为真正有趣的惰性应用不仅有一个单一的函数参数惰性(这不会给你带来太多好处),而是将惰性值分散到一个深层数据结构的主干中。最后,它不仅仅是一个 delay-function 你在进入惰性函数之后评估的,它是对这些函数的嵌套调用的整个 torrent(实际上,可能是无限多! ) 因为惰性数据结构被消耗了。
因此,Scala 通过默认不做任何惰性操作来避免这种危险,尽管正如 Alec 所说,它确实提供了一个 lazy
关键字,该关键字基本上将 memoised-function 包装器添加到值。
这可能很有用,但不适合发表评论。
您可以在 Scala 中编写一个函数,其行为类似于 Haskell 的 call-by-need(对于参数),方法是将参数设置为 call-by-name 并在函数开始时对它们进行惰性计算:
def foo(x: => Int) = {
lazy val _x = x
// make sure you only use _x below, not x
}
Haskell 的懒惰求值 will never 比急切求值需要更多的求值步骤。
另一方面,Scala 的按名称调用评估 may require 评估步骤比按值调用多(如果短路收益被重复计算的成本抵消)。
我认为按名称调用大致等同于惰性求值。那么为什么在时间保证上会有这样的差异呢?
我猜测可能 Haskell 语言指定在评估期间必须使用记忆;但在那种情况下,为什么 Scala 不这样做呢?
评估策略的名称有一定的广度,但大致可以归结为:
按名称调用 参数几乎只是以调用函数时的任何(未评估)形式替换到函数体中。这意味着它可能需要在体内进行多次评估。
在 Scala 中,您可以这样写:
scala> def f(x:=> Int): Int = x + x scala> f({ println("evaluated"); 1 }) evaluated evaluated 2
在 Haskell 中,您没有内置的方法来执行此操作,但您始终可以将 call-by-name 值表示为类型
() -> a
的函数。不过,由于引用透明性,这有点模糊——您将无法像使用 Scala 那样测试它(并且编译器可能会优化您调用的 "by name" 部分)。call by need(懒惰......有点)调用函数时不评估参数,但在第一次需要时.在那一刻,它也被缓存了。之后,每当再次需要该参数时,就会查找缓存的值。
在 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 互操作而构建的,因此它需要支持命令式 side-effectful 编程。因此,在许多情况下 不是 在 Scala 中使用 lazy
的好主意。
此外,lazy
有性能开销:您需要有一个额外的间接寻址来检查该值是否已经被计算过。在 Scala 中,这意味着更多的对象,这给垃圾收集器带来了更大的压力。
最后,在某些情况下,惰性求值会留下 "space" 漏洞。例如,在 Haskell 中,通过将它们相加从右边折叠一大列数字是一个坏主意,因为 Haskell 将在计算之前建立对 (+)
的这个巨大系列惰性调用他们(实际上你只需要它来有一个累加器。一个著名的 space 问题的例子是 foldr
vs foldl
vs foldl'
.
我不知道为什么 Scala 没有结果 有 “适当的”惰性求值——可能它的实现并不那么简单,尤其是当您希望语言与 JVM 顺利交互时。
Call-by-name(如您所见)不等同于惰性求值,而是用 () -> a
类型的参数替换 a
类型的参数。这样的函数包含与普通 a
值(类型是同构的)相同数量的信息,但要实际获得该值,您始终需要将函数应用于 ()
伪参数。当您对该函数求值两次时,您将得到 twice the same result, but it must each time be calculated anew (since automatically memoising functions is not feasible).
惰性求值等同于用行为类似于以下 OO class:
的类型的参数替换类型a
的参数
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);
}
}
这本质上只是围绕特定 call-by-name– 函数类型的 memoisation-wrapper。不太好的是,这个包装器以一种基本方式依赖于 side-effects:当第一次评估惰性值时,您必须改变 containedValue
以表示该值现在已知的事实。 Haskell 在其运行时的核心有这种机制 hard-baked,well-tested 用于 thread-safety 等等。但是在一种试图尽可能公开地使用命令式 VM 的语言中,如果这些虚假突变与显式 side-effects 交织在一起,可能会引起严重的头痛。特别是,因为真正有趣的惰性应用不仅有一个单一的函数参数惰性(这不会给你带来太多好处),而是将惰性值分散到一个深层数据结构的主干中。最后,它不仅仅是一个 delay-function 你在进入惰性函数之后评估的,它是对这些函数的嵌套调用的整个 torrent(实际上,可能是无限多! ) 因为惰性数据结构被消耗了。
因此,Scala 通过默认不做任何惰性操作来避免这种危险,尽管正如 Alec 所说,它确实提供了一个 lazy
关键字,该关键字基本上将 memoised-function 包装器添加到值。
这可能很有用,但不适合发表评论。
您可以在 Scala 中编写一个函数,其行为类似于 Haskell 的 call-by-need(对于参数),方法是将参数设置为 call-by-name 并在函数开始时对它们进行惰性计算:
def foo(x: => Int) = {
lazy val _x = x
// make sure you only use _x below, not x
}