Haskell,无限列表上的折叠器

时间:2018-02-12 21:12:30

标签: haskell type-inference fold

我在读LYAH时遇到了一个问题。

以下是我对无限列表中的foldr的看法:

foldr (:) [] [1..] = 1:2:...:**∞:[]**

我认为GHCi在评估∞:[] 之前并不知道它是列表。

但GHCi知道。

所以我认为它可以识别foldr(:) [] [无限列表] = [无限列表本身]。

Prelude> [1..10] == (take 10 $ foldr (:) [] [1..])
True
Prelude> [1..] == (foldr (:) [] [1..])
Interrupted.

然而事实并非如此。

我想知道在评估∞:[]之前GHCi识别它是[1 ..]时实际发生了什么。

只需在评估之前输入推理?

2 个答案:

答案 0 :(得分:5)

  

我想知道在评估[1..]之前GHCi识别∞:[]时实际发生了什么。

GHCi 认识到它是[1...],这只是 lazy 评估的结果。

foldr实现为:

foldr _ z [] = z
foldr f z (x:xs) = f x (foldr f z xs)

如果你写的是foldr (:) [] [1..]之类的东西,那么Haskell会宣传这个(直接),它只存储你想要计算的东西。

现在说你想要print (take 3 (foldr (:) [] [1..]))那个列表,然后Haskell被迫评估它,它将通过计算来实现:

take 3 (foldr (:) [] [1..])
-> take 3 ((:) 1 (foldr (:) [] [2..]))
-> (:) 1 (take 2 (foldr (:) [] [2..]))
-> (:) 1 (take 2 ((:) 2 (foldr (:) [] [3..]))
-> (:) 1 ((:) 2 (take 1 (foldr (:) [] [3..])))
-> (:) 1 ((:) 2 (take 1 ((:) 3 (foldr (:) [] [4..]))))
-> (:) 1 ((:) 2 ((:) 3 (take 0 (foldr (:) [] [4..]))))
-> (:) 1 ((:) 2 ((:) 3 [])

因此它派生[1, 2, 3],并且由于Haskell的懒惰,它对foldr (:) [] [4..]的内容不感兴趣。即使该列表最终会停止,也不会对其进行评估。

如果计算[1..] = foldr (:) [] [1..]之类的东西,那么Haskell将检查列表相等性,列表相等性定义为:

[] == [] = True
(x:xs) == (y:ys) = x == y && xs == ys
[] == (_:_) = False
(_:_) == [] = False

所以Haskell被迫展开右侧foldr的列表,但它会继续这样做,直到它找到不相等的项目,或者列表中的一个到达结束。但是因为每次元素 相等,并且两个列表都永远不会结束,所以它永远不会完成,因为它会像下面那样评估它:

   (==) [1..] (foldr (:) [] [1..])
-> (==) ((:) 1 [2..])  ((:) 1 (foldr (:) [] [2..]))

它看到两者都是相同的,所以它递归地调用:

-> (==) ((:) 1 [2..])  ((:) 1 (foldr (:) [] [2..]))
-> (==) [2..] foldr (:) [] [2..])
-> (==) ((:) 2 [3..])  ((:) 2 (foldr (:) [] [3..]))
-> (==) [3..] foldr (:) [] [3..])
-> ...

但正如你所看到的,它永远不会停止评估。 Haskell不知道 foldr (:) [] [1..]等于[1..],它的目的是评估它,并且由于等式迫使它评估整个列表,它将陷入无限循环。

是的,可以在编译器中添加某个模式,这样foldr (:) [] x将替换为x,因此将来也许Haskell编译器可以返回True这些,但这不能解决问题从根本上,因为如果Haskell可以为任何类型的函数(这里(:))派生这样的东西,那么它将解决一个不可判定的问题,因此它不是可能的)。

答案 1 :(得分:2)

Ghc(至少在理论上)不知道有限列表和无限列表之间的区别。它可以通过计算列表长度来判断列表是否有限。如果你试图找到一个无限列表的长度,你将会遇到一个糟糕的时间,因为你的程序永远不会终止。

这个问题实际上是关于懒惰的评价。在像C或python这样严格的语言中,你需要知道每一步的整体价值。如果你想要列出一个列表的元素,你需要知道它的内容和开始之前有多少。

Haskell中的所有数据都具有以下形式:

  1. 原始的完全已知的东西,如int(不一定是整数)
  2. 数据类型构造函数及其参数,例如TrueLeft 7(,) 5 'f'(与(5,'f')相同)或(:) 3 []
  3. 但在Haskell中,值有两个“形状”

    • 已知并完全评估(如在C或python中)
    • 尚未评估 - 一个函数“thunk”,当你调用时会以更加评估的方式返回值

    在Haskell中有一个名为弱头正常形式的概念,其中:

    • 任何原始内容都经过全面评估
    • 对于任何带有构造函数的东西,构造函数都是已知的,参数可能尚未被评估。

    让我们看一下foldr (:) [] [1..]的评估过程。首先是foldr

    的定义
    foldr f a [] = a
    foldr f a (x:xs) = f x (foldr xs)
    

    现在什么是foldr (:) [] [1..]

    foldr (:) [] [1..]
    

    这似乎只是一个笨蛋。我们对此一无所知。所以让我们把它评估成WHNF。首先,我们需要将参数[1..](实际上是enumFrom 1)转换为WHNF,以便我们可以对其进行模式匹配:

    foldr (:) [] (1:[2..])
    

    现在我们可以评估foldr:

    (:) 1 (foldr [] [2..])
    1 : (foldr [] [2..])
    

    因此,我们计算了列表的第一个元素,而不必查看其整个无限长度。同样,我们可以计算出第二个元素,依此类推。

    那么如果我们[1..] == [1..]会怎么样?那么列表==的定义是(省略三种情况)

    (x:xs) == (y:ys) = x == y && xs == ys
    

    因此,我们试图减少到WHNF:

    [1..] == [1..]
    (1 == 1) && ([2..] == [2..])
    True && ([2..] == [2..])
    [2..] == [2..]
    ... and so on
    

    因此,我们继续前进,永远不会得到一个构造函数,我们可以用它来模拟匹配(即检查)结果。

    请注意,我们可以取消True && ...,因为&&的定义不会查看其第二个参数:

    True && x = x
    False && _ = False
    

    如果我们用一个完整的四向真值表定义&&,那么程序的内存可能会快得多(假设编译器没有做任何聪明的事情)而不是上面的内容,相反,你将会用尽耐心(或宇宙射击击中你的ram并让你的程序返回False