【问题标题】:Why is it sometimes possible to fold an infinite list from the right?为什么有时可以从右侧折叠无限列表?
【发布时间】:2020-12-13 00:29:26
【问题描述】:

当我被困在作业 6 的第 5 部分时,我一直在经历出色的 CIS 194 course。它围绕着在没有任何可分性测试的情况下实现 ruler function

我发现可以通过不断地将累加器与无限列表中的值穿插来构建标尺函数。

nats = [0,1,2,3,..]
[3]
[2,3,2]
[1,2,1,3,1,2,1]
[0,1,0,2,0,1,0,3,0,1,0,2,0]

然后我尝试为Stream 数据类型实现此算法,这是一个没有nil 的列表

data Stream a = Cons a (Stream a)

streamToList :: Stream a -> [a]
streamToList (Cons x xs) = x : streamToList xs

instance Show a => Show (Stream a) where
  show = show . take 20 . streamToList

streamFromSeed :: (a -> a) -> a -> Stream a
streamFromSeed f x = Cons x (streamFromSeed f (f x))

nats :: Stream Integer
nats = streamFromSeed succ 0

interleave x (Cons y ys) = Cons x (Cons y (interleave x ys))
foldStream f (Cons x xs) = f x (foldStream f xs)
ruler = foldStream interleave nats

正如预期的那样,由于我试图从右侧折叠,所以我遇到了 stackoverflow 错误。但是,我很惊讶地看到相同的算法适用于普通的无限列表。

import Data.List

interleave x list = [x] ++ (intersperse x list) ++ [x]
ruler = take 20 (foldr interleave [] [0..])

我错过了什么?为什么一种实现有效而另一种无效?

【问题讨论】:

  • foldr 不完全从右侧折叠,它关联到右侧,即foldr f z [1 .. 3] = 1 `f` (2 `f` (3 `f` z)) = f 1 $ f 2 $ f 3 $ z。如果f 在其第二个参数中是惰性的,它可以在无限流上工作并逐渐产生结果。同样,懒惰的foldl(而不是严格的foldl')可以在其第一个参数中使用懒惰的函数。问题在于使用了错误的严格性——在参数 1 到 foldl 或在参数 2 到 foldr 中给出函数 strict——所以它需要一直强制到流的末尾才能开始产生结果。对于无限的流,没有这样的结束。

标签: haskell lazy-evaluation


【解决方案1】:

您的interleave 不够懒惰。正确折叠在无限结构上工作所必须做的神奇事情是在它们进行第一次计算之前不要太仔细地检查折叠值的结果。所以:

interleave x stream = Cons x $ case stream of
    Cons y ys -> Cons y (interleave x ys)

这会在检查 stream 之前生成 Cons x _;相比之下,您的版本需要对 stream 进行一些评估,然后才能传递到等式的右侧,这实际上强制整个折叠发生在任何构造函数生成之前。

您还可以在interleave 的列表版本中看到这一点:

interleave x list = [x] ++ intersperse x list ++ [x]

返回列表的第一个元素 (x) 在 intersperse 开始对 list 进行模式匹配之前是已知的。

【讨论】:

    【解决方案2】:

    我们可以查看foldr [src]的源代码。噪音较小的版本如下所示:

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

    Haskell 热切地评估。因此,这意味着,除非您需要 (foldr f z xs),否则它不会评估累加器。这意味着f不需要第二个参数,例如因为第一项x有一定的值,它不会计算累加器。

    例如如果我们实现takeWhileNeq:

    takeWhileNeq a = foldr f []
        where f x xs -> if x == a then [] else (x:xs)
    

    如果我们因此在列表takeWhileNeq 2 [1,4,2,5] 上运行它,那么它将不会评估任何东西。但是,如果我们想打印结果,它将评估为:

       f 1 (foldr f [4,2,5])
    

    f 会检查1 == 2,因为不是这样,它会返回(x:xs),所以:

    -> 1 : foldr f [4,2,5]
    

    所以现在它会评估4 == 2,因为这是错误的,它会评估这个:

    -> 1 : (4 : foldr f [2,5])
    

    现在我们计算2 == 2,因为这是True,函数返回空列表,ingores是累加器,所以它永远不会查看foldr f [5]

    -> 1 : (4 : [])
    

    对于无限列表,它也会因此产生一个空列表并忽略折叠列表的其余部分。

    【讨论】:

      猜你喜欢
      • 2016-05-31
      • 2011-11-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-06-22
      • 2018-04-13
      • 1970-01-01
      • 2013-03-13
      相关资源
      最近更新 更多