Haskell 不会懒惰地评估交错

Posted

技术标签:

【中文标题】Haskell 不会懒惰地评估交错【英文标题】:Haskell not lazy evaluating interleaving 【发布时间】:2014-08-29 03:04:17 【问题描述】:

我正在解决一个问题(它来自一个 UPenn 类,但我没有接受它(只是通过它来学习 Haskell)),重点是构造一个 Stream(如下定义),它是定义的通过“标尺”(标尺!!n = 2 的最高幂的指数,将除以 n)。问题是我认为下面的标尺定义应该懒惰地评估,但它似乎是严格评估并无限运行。如果我通过添加像“nthStream 10 = streamRepeat 10”这样的终端案例来限制它,它会运行并生成输出到我想要正确的点。

data Stream a = Stream a (Stream a)

streamToList :: Stream a -> [a]
streamToList (Stream a rest) = [a] ++ streamToList rest

instance Show a => Show (Stream a) where
    show s = show $ take 100 $ streamToList s

streamRepeat :: a -> Stream a
streamRepeat a = Stream a (streamRepeat a)

interleaveStreams :: Stream a -> Stream a -> Stream a
interleaveStreams (Stream a arest) (Stream b brest) = (Stream a (Stream b (interleaveStreams arest brest)))

nthStream :: Integer -> Stream Integer

nthStream n = interleaveStreams (streamRepeat n) (nthStream (n+1))

ruler :: Stream Integer
ruler = nthStream 0 

谁能解释为什么统治者(和 nthStream)不懒惰地评估?

【问题讨论】:

不应该有streamRepeat的基本情况吗? @shree.pat18 streamRepeat 应该返回一个无限流,如果我没记错的话,很像 Prelude 中的 repeat :: a -> [a] bheklir,你是对的。它应该返回一个无限流。例如:streamRepeat 3 = Stream 3 (Stream 3 (Stream 3.... @shree.pat18 我不确定我理解您所说的“终端情况”或“明确定义”是什么意思。导致它终止的原因是请求了多少无限数据结构,而不是问题中定义的任何函数。 @shree.pat18 你在考虑递归,但这是corecursion。 【参考方案1】:

我会尝试将您推向正确的方向,或者至少指出问题所在。函数nthStream 永远不会计算,甚至不会计算它的第一个元素,因为它是如何用interleaveStreams 定义的。举个例子,让我们计算出它对nthStream 0 的评估结果(为了简洁和可读性,我将nthStream 重命名为nthinterleaveStream 重命名为interleavestreamRepeat 重命名为repeat,和StreamStrm):

nth 0 = interleave (repeat 0) (nth 1)
      = interleave (Strm 0 _0) (interleave (repeat 1) (nth 2))
      = interleave (Strm 0 _0) (interleave (Strm 1 _1) (nth 2))
      = interleave (Strm 0 _0) (interleave (Strm 1 _1) (interleave (repeat 2) (nth 3)))
      = interleave (Strm 0 _0) (interleave (Strm 1 _1) (interleave (Strm 2 _2) (nth 3)))

我选择将从repeat 返回的每个流的尾部表示为_N,其中N 是重复的数字。这些当前是 thunk,因为我们还没有请求它们的值。请注意正在构建的是interleaves 链,而不是Strm 构造函数链。我们得到了我们感兴趣的每一个,但在评估第二个参数之前,它们永远无法从interleave 返回。由于第二个参数总是被简化为对interleave 的新调用,因此它永远不会减少。

这里有一个提示:你如何递归地定义interleaveStreams,以便它只依赖于它的第一个参数已经被构造?即

interleaveStreams (Stream a arest) streamB = Stream a (???)

【讨论】:

这行得通吗? interleaveStreams (Stream a arest) streamB = Stream a (interleaveStreams streamB arest) ? @BrettJurman 我不知道,你告诉我。如果你做take 10 $ streamToList $ nthStream 0,这会让nthStream 评估吗? interleaveStream 是否仍然采用两个流并将它们正确交错在一起? @BrettJurman 基本上,我的意思是你已经找到了一个可能的解决方案,所以测试一下吧!它是否给您预期的响应? 它似乎确实有效。谢谢!抱歉,我没有测试它,我想睡觉,但我觉得我应该回应,因为你很有帮助【参考方案2】:

你的问题是线路

interleaveStreams (Stream a arest) (Stream b brest) = (Stream a (Stream b (interleaveStreams arest brest)))

为了使该函数甚至返回结果的开头,两个都必须对其参数进行评估,因为您直接在它们的构造函数上进行模式匹配。但是你在使用它

nthStream n = interleaveStreams (streamRepeat n) (nthStream (n+1))

这意味着nthStream n 在评估nthStream (n+1) 之前无法返回任何东西,这会为您提供无限递归。

要解决此问题,您可以将有问题的行中的第二个模式更改为显式 lazy 并使用 ~:

interleaveStreams (Stream a arest) ~(Stream b brest) = (Stream a (Stream b (interleaveStreams arest brest)))

【讨论】:

我什至不知道“显式”懒惰的概念。鉴于这行得通,似乎我应该能够在没有它的情况下以某种方式编写它?是否有替代方案可以或多或少满足我的要求?我可能会感到困惑,但谢谢! @BrettJurman 当然有一种方法可以在没有无可辩驳的模式的情况下做到这一点,我希望你能从我的回答中弄清楚;)。除了对第二个参数进行模式匹配之外,有没有一种方法可以定义interleaveStreams,您不需要立即需要每个流的两个头?检查我的答案以获得稍微更新的提示。 @bheklilr 我在您的回答线程中对此作出了回应。感谢您的提示!

以上是关于Haskell 不会懒惰地评估交错的主要内容,如果未能解决你的问题,请参考以下文章

在 Haskell 中交错列表列表

是否有使用严格评估的 Haskell 编译器或预处理器?

Haskell中的懒惰和尾递归,为啥会崩溃?

seq如何评估Haskell中的无限列表?

Haskell如何强制在haskell中评估Data.Map?

素数的惰性列表