为什么Haskell';s foldr不是stackoverflow,而Scala实现是?
我正在读书 练习3.10说明为什么Haskell';s foldr不是stackoverflow,而Scala实现是?,scala,haskell,functional-programming,fold,Scala,Haskell,Functional Programming,Fold,我正在读书 练习3.10说明foldRight溢出(见下图)。 但据我所知,Haskell中的foldr却没有 这种不同的行为是如何可能的 导致这种不同行为的两种语言/编译器之间的差异是什么 这种差异从何而来?站台?语言?编译器 可以在Scala中编写堆栈安全foldRight吗?如果是,如何进行 哈斯克尔很懒。因此foldr在堆上分配,而不是在堆栈上。根据参数函数的严格程度,它可以分配单个(小)结果,也可以分配大结构 与严格的尾部递归实现相比,您仍然在损失空间,但这看起来并不明显,因为您已
foldRight
溢出(见下图)。
但据我所知,Haskell中的foldr却没有 这种不同的行为是如何可能的 导致这种不同行为的两种语言/编译器之间的差异是什么 这种差异从何而来?站台?语言?编译器 可以在Scala中编写堆栈安全foldRight吗?如果是,如何进行
哈斯克尔很懒。因此
foldr
在堆上分配,而不是在堆栈上。根据参数函数的严格程度,它可以分配单个(小)结果,也可以分配大结构
与严格的尾部递归实现相比,您仍然在损失空间,但这看起来并不明显,因为您已经用堆栈交换了堆。Haskell很懒惰。定义
foldr f z (x:xs) = f x (foldr f z xs)
告诉我们,具有非空列表的foldr f z xs
的行为是由组合函数f
的惰性决定的
特别是调用foldr f z(x:xs)
只在堆上分配一个thunk,{foldr f z xs}
(为包含表达式的thunk写入{…}
),并使用两个参数调用f
,以及thunk。接下来发生的事情是f
的责任
特别是,如果它是一个惰性数据构造函数(例如,(:)
),它将立即返回给foldr
调用的调用者(构造函数的两个插槽由(引用)这两个值填充)
如果f
确实需要其右边的值,则在最小的编译器优化下,不应创建任何thunk(或者最多创建一个thunk-当前thunk),因为立即需要foldr f z xs
的值,并且可以使用通常的基于堆栈的求值:
foldr f z [a,b,c,....,n] ==
a `f` (b `f` (c `f` (... (n `f` z)...)))
因此,当与超长输入列表上的严格组合函数一起使用时,foldr确实会导致这种情况。但是,如果组合函数不立即要求其右侧的值,或者只要求其一部分,则计算将暂停,并立即返回由
f
创建的部分结果。与左边的参数相同,但它们可能在输入列表中以thunk的形式出现。请注意,这里的作者并不是指scala标准库中的任何foldRight定义,例如列表中定义的foldRight定义。他们参考了上文第3.4节中给出的foldRight定义
scala标准库根据foldLeft定义foldRight,方法是反转列表(可以在常量堆栈空间中完成),然后调用foldLeft并反转传递函数的参数。这适用于列表,但不适用于无法安全反转的结构,例如:
scala> Stream.continually(false)
res0: scala.collection.immutable.Stream[Boolean] = Stream(false, ?)
scala> res0.reverse
java.lang.OutOfMemoryError: GC overhead limit exceeded
现在让我们思考一下此操作的结果:
Stream.continually(false).foldRight(true)(_ && _)
答案应该是假的,不管流中有多少假值,或者它是无限的,如果我们将它们与一个连词组合,结果将是假的
haskell当然可以毫无问题地得到这个:
Prelude> foldr (&&) True (repeat False)
False
这是因为两件重要的事情:haskell的foldr将从左到右而不是从右到左遍历流,而haskell在默认情况下是懒惰的。这里的第一项,foldr实际上从左到右遍历列表,可能会让一些认为右折叠是从右开始的人感到惊讶或困惑,但右折叠的重要特征不是它从结构的哪一端开始,而是关联性在哪个方向。因此,给出一个列表[1,2,3,4]和一个名为op
,左折为
((1 op 2) op 3) op 4)
右边的褶皱是
(1 op (2 op (3 op 4)))
但评估的顺序不重要。因此,作者在第3章中所做的是给你一个从左到右遍历列表的折叠,但是由于scala默认是严格的,我们仍然无法遍历我们的无限错误流,但是有一些耐心,他们会在第5章中做到:)我会给你一个偷看,让我们看看标准库中定义的foldRight与scalaz中可折叠typeclass中定义的foldRight之间的区别:
下面是scala标准库的实现:
def foldRight[B](z: B)(op: (A, B) => B): B
以下是scalaz的可折叠文件的定义:
def foldRight[B](z: => B)(f: (A, => B) => B): B
不同之处在于Bs都是惰性的,现在我们可以再次折叠无限流,只要我们给出一个第二个参数足够惰性的函数:
scala> Foldable[Stream].foldRight(Stream.continually(false),true)(_ && _)
res0: Boolean = false
在Haskell中演示这一点的一个简单方法是使用等式推理来演示惰性评估。让我们根据
foldr
编写find
函数:
-- Return the first element of the list that satisfies the predicate, or `Nothing`.
find :: (a -> Bool) -> [a] -> Maybe a
find p = foldr (step p) Nothing
where step pred x next = if pred x then Just x else next
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
在一种急切的语言中,如果您使用foldr
编写find
,它将遍历整个列表并使用O(n)空间。对于惰性计算,它在满足谓词的第一个元素处停止,并且只使用O(1)空间(模垃圾收集):
尽管列表[0..]
是无限的,但此计算会在有限的步骤中停止,因此我们知道我们不会遍历整个列表。此外,每一步表达式的复杂度都有一个上限,这将转化为计算该值所需内存的恒定上限
这里的关键是我们正在折叠的step
函数具有以下属性:无论x
和next
的值是什么,它都将:
仅x
,而不调用下一个thunk,或
next
thunk(实际上,如果不是字面意思的话)Haskell中的foldr不是尾部递归的,所以我
-- Return the first element of the list that satisfies the predicate, or `Nothing`.
find :: (a -> Bool) -> [a] -> Maybe a
find p = foldr (step p) Nothing
where step pred x next = if pred x then Just x else next
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
find odd [0..]
== foldr (step odd) Nothing [0..]
== step odd 0 (foldr (step odd) Nothing [1..])
== if odd 0 then Just 0 else (foldr (step odd) Nothing [1..])
== if False then Just 0 else (foldr (step odd) Nothing [1..])
== foldr (step odd) Nothing [1..]
== step odd 1 (foldr (step odd) Nothing [2..])
== if odd 1 then Just 1 else (foldr (step odd) Nothing [2..])
== if True then Just 1 else (foldr (step odd) Nothing [2..])
== Just 1