Haskell 无限列表上的元组解构在将元组作为参数解构时与使用 let 解构时的行为不同

Haskell Tuple destructuring on infinite lists behaves differently when destructuring the Tuple as an argument than when destructuring using let

在尝试使用 foldr 实现 dropWhile 时,我想出的第一个算法是这个

dropWhile' :: (a -> Bool) -> [a] -> [a]
dropWhile' pred = fst . foldr (\cur (acc, xs) ->
    if pred cur
    then (acc, cur:xs)
    else (cur:xs, cur:xs)) ([], [])

虽然这行得通,但它会在不提供任何值的情况下导致无限列表发生堆栈溢出。 因为我不确定,为什么这行不通,所以我一直在玩这个函数,直到我想出这个:

dropWhile' :: (a -> Bool) -> [a] -> [a]
dropWhile' pred = fst . foldr (\cur t -> let (acc, xs) = t in
    if pred cur
    then (acc, cur:xs)
    else (cur:xs, cur:xs)) ([], [])

如您所见,这一个与第一个之间的唯一区别是,我在这里解构 let 绑定中的元组 (acc, xs),而不是直接在函数参数内部解构。出于某种奇怪的原因,此代码适用于无限列表。 如果有人知道为什么会这样,请告诉我。

让 Haskell 中的表达式是惰性的:

Let Expressions

(...) Pattern bindings are matched lazily; an implicit ~ makes these patterns irrefutable.

相比之下,lambda 抽象脱糖为 case,从而使构造函数模式变得严格:

Curried applications and lambda abstractions

Translation: The following identity holds:

\ p1 … pn -> e = \ x1 … xn -> case (x1, …, xn) of (p1, …, pn) -> e

粗略地说,

\cur (acc, xs) -> use cur acc xs

在计算 use ... 之前强制立即计算第二个参数。 在 foldr 中,这会在执行任何其他操作之前处理列表的尾部。如果列表是无限的,我们将陷入无限递归。

同样适用于

\cur t -> case t of (acc, xs) -> use cur acc xs

相反,

\cur t -> use cur (fst t) (snd t)

不会立即强制计算第二个参数 t,因为 Haskell 是惰性的。只有当 use 实际上需要它的第二个或第三个参数时, t 的计算才会触发。

等价地,我们可以写

\cur t -> case t of ~(acc, xs) -> use cur acc xs
-- or
\cur ~(acc, xs) -> use cur acc xs

同样的效果,使用 lazy/irrefutable 模式来延迟第二个参数的解构。当使用 letwhere 时,(顶级)模式是隐式惰性的,所以我们也可以写

\cur t -> let (acc, xs) = t in use cur acc xs