为什么Haskell的折叠器不是stackoverflow而是同一个Scala实现呢?

时间:2014-09-02 12:02:43

标签: scala haskell functional-programming fold

我正在阅读FP in Scala

练习3.10说foldRight溢出(见下图)。 据我所知,然而Haskell中foldr没有。

http://www.haskell.org/haskellwiki/

-- if the list is empty, the result is the initial value z; else
-- apply f to the first element and the result of folding the rest
foldr f z []     = z 
foldr f z (x:xs) = f x (foldr f z xs) 

-- if the list is empty, the result is the initial value; else
-- we recurse immediately, making the new initial value the result
-- of combining the old initial value with the first element.
foldl f z []     = z                  
foldl f z (x:xs) = foldl f (f z x) xs

这种不同的行为怎么可能?

造成这种不同行为的两种语言/编译器有什么区别?

这种差异来自哪里?该平台 ?语言?编译器?

是否可以在Scala中编写堆栈安全的foldRight?如果是,怎么样?

enter image description here

enter image description here

4 个答案:

答案 0 :(得分:19)

Haskell很懒。定义

foldr f z (x:xs) = f x (foldr f z xs)

告诉我们foldr f z xs带有非空列表xs的行为取决于合并函数f laziness

特别是调用foldr f z (x:xs)只在堆上分配一个thunk, {foldr f z xs} (写 {...} 进行thunk控件表达式...),并使用两个参数f和thunk调用x。接下来会发生什么,f的责任。

特别是,如果它是一个懒惰的数据构造函数(例如(:)),它将立即返回给foldr调用的调用者(使用构造函数的两个)插槽填充(引用)两个值。)

如果f确实在右边需要它的值,那么只需要最少的编译器优化就不应该创建任何thunk(或者一个,最多 - 当前的那个),作为{{1}的值立即需要并且可以使用通常的基于堆栈的评估:

foldr f z xs

因此,当在极长输入列表上使用严格组合函数时,foldr f z [a,b,c,....,n] == a `f` (b `f` (c `f` (... (n `f` z)...))) 确实可以导致SO。但是如果组合函数没有立即要求它的值,或者只需要它的一部分,则评估将暂停在thunk中,而foldr创建的部分结果将是马上回来了。与左侧的参数相同,但它们可能已经在输入列表中显示为thunk。

答案 1 :(得分:18)

Haskell很懒。所以foldr在堆上分配,而不是堆栈。根据参数函数的严格性,它可以分配单个(小)结果或大结构。

与严格的尾递归实现相比,你仍然失去了空间,但它看起来并不那么明显,因为你已经堆叠了堆栈。

答案 2 :(得分:5)

请注意,这里的作者并未引用scala标准库中的任何foldRight定义,例如List上定义的定义。它们指的是他们在3.4节中给出的foldRight的定义。

scala标准库通过反转列表(可以在常量堆栈空间中完成)来定义foldRft的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是懒惰的。这里的第一个项目,折叠器实际上从左到右遍历列表可能会让一些想到正确折叠的人从右边开始感到惊讶或混淆,但右折叠的重要特征不是它开始的结构的哪一端在,但在相关性的方向。所以给出一个列表[1,2,3,4]和一个名为op的操作,左侧是

((1 op 2) op 3) op 4)

右折是

(1 op (2 op (3 op 4)))

但评估的顺序并不重要。所以作者在第3章中所做的是给你一个从左到右遍历列表的折叠,但是因为scala默认是严格的,所以我们仍然无法遍历我们的无限谬误流,但是有一些耐心,他们将在第5章中找到它:)我将给你一个先睹为快,让我们看一下在标准库中定义的foldRight与在scalaz中的可折叠类型类中定义的区别:

这里是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

答案 3 :(得分:4)

在Haskell中演示这一点的一种简单方法是使用等式推理来演示延迟评估。让我们按find

编写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)

用热切的语言,如果你用findfoldr,它将遍历整个列表并使用O(n)空格。使用惰性求值,它会在满足谓词的第一个元素处停止,并且仅使用O(1)空间(模数垃圾回收):

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

尽管列表[0..]是无限的,但是这个评估在有限的步骤中停止,因此我们知道我们没有遍历整个列表。此外,每个步骤的表达式的复杂性都有一个上限,这将转化为评估此内容所需的内存的常量上限。

这里的关键是我们正在折叠的step函数具有以下属性:无论xnext的值是什么,它都将:

  1. 在不调用Just x thunk或
  2. 的情况下评估next
  3. 尾调用next thunk(实际上,如果不是字面意思)。