4

When trying to implement dropWhile using foldr the first algorithm that I came up with was this

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

While this works, it causes a stack overflow on infinite lists without giving any values. Because I wasn't sure, why this doesn't work, I just played around with the function until I came up with this:

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)) ([], [])

As you can see, the only difference between this one and the first is, that here I'm destructuring the tuple (acc, xs) inside a let binding instead of destructuring directly inside the function parameter. For some weird reason, this code works on infinite lists. If anyone has any idea why this behaves as it does, please let me know.

Prophet
  • 186
  • 8

2 Answers2

7

Let expressions in Haskell are lazy:

Let Expressions

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

In contrast, lambda abstractions desugar to case, thus making constructor patterns strict:

Curried applications and lambda abstractions

Translation: The following identity holds:

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

Li-yao Xia
  • 31,896
  • 2
  • 33
  • 56
  • 2
    ...and you can use the hinted-at `~` to make the lambda's pattern explicitly lazy and get the best of both world's (`let`'s good behavior on infinite lists and lambda's better aesthetics). `\cur ~(acc, xs) -> if ...` – Daniel Wagner Jun 12 '20 at 23:10
3

Roughly put,

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

forces the evaluation of the second argument immediately, before evaluating use .... Inside a foldr, this processes the tail of the list before doing anything else. If the list is infinite, we will get stuck in an infinite recursion.

The same holds for

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

Instead,

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

does not immediately force the evaluation of the second argument t, since Haskell is lazy. Only when use actually needs its second or third argument the evaluation of t will trigger.

Equivalently, we can write

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

to the same effect, using a lazy/irrefutable pattern to delay the destructuring of the second argument. When using let and where, (top-level) patterns are implicitly lazy, so we can also write

\cur t -> let (acc, xs) = t in use cur acc xs
chi
  • 111,837
  • 3
  • 133
  • 218