2

I'm bit stuck how to rewrite following strict-evaluated list comprehension to use seq instead of bang pattern:

zipWith' f l1 l2 = [ f e1 e2 | (!e1, !e2) <- zip l1 l2 ]

Any idea ?

I've tried

zipWith' f l1 l2 = [ e1 `seq` e2 `seq` f e1 e2 | (e1, e2) <- zip l1 l2 ]

but this unfortunately does not force an evaluation to WHNF.

hammar
  • 138,522
  • 17
  • 304
  • 385
David Unric
  • 7,421
  • 1
  • 37
  • 65

3 Answers3

6

You can mechanically translate bang patterns into calls to seq following the GHC manual:

This:

zipWith' f l1 l2 = [ f e1 e2 | (!e1, !e2) <- zip l1 l2 ]

Becomes Too lazy:

zipWith' f l1 l2 =
    [ f e1 e2
    | e <- zip l1 l2
    , let t = case e of (x,y) -> x `seq` y `seq` (x,y)
    , let e1 = fst t
    , let e2 = snd t
    ]

Which is more concisely written as (too lazy):

zipWith' f l1 l2 =
    [ f e1 e2
    | e <- zip l1 l2
    , let (e1,e2) = case e of (x,y) -> x `seq` y `seq` (x,y)
    ]

Though I'd write it as (wrong, too lazy)

zipWith' f l1 l2 = zipWith (\x y -> uncurry f (k x y)) l1 l2
    where
        k x y = x `seq` y `seq` (x, y)

You could also move the strictness hint to a data structure:

data P = P !Integer !Integer

zipWith' f l1 l2 = [ f e1 e2 | P e1 e2 <- zipWith P l1 l2 ]

Or as:

zipWith' f l1 l2 = [ f e1 e2 | (e1, e2) <- zipWith k l1 l2 ]
    where
        k x y = x `seq` y `seq` (x,y)
Don Stewart
  • 137,316
  • 36
  • 365
  • 468
  • 1
    Unfortunately none of the above versions of zipWith' function look strict. If run in expression fibs !! (10^6) where fibs = 0:1:zipWith' (+) fibs (tail fibs) aborts with "Stack space overflow" error. The original version with bang-patterns finishes without Stack overflow. In addition the last example not even compile: Couldn't match expected type `Int' with actual type `(t0, t1)' Expected type: (t0, t1) -> (t0, t1) -> Int Actual type: (t0, t1) -> (t0, t1) -> (t0, t1) In the first argument of `zipWith'', namely `(+)' – David Unric Jun 26 '11 at 21:33
  • I've added a couple of other examples that work, though am pretty surprised the translated ones are too lazy, as you point out. If someone wants to walk through the strictness here, please do. – Don Stewart Jun 26 '11 at 22:14
  • Thanks Don, the last sample is closest answer to my question, although I'd expect even more concise code. Any idea why you've got lazy versions by direct translation of bang-patterns definition ? Is there some bug in GHC manual or is just simply outdated ? – David Unric Jun 27 '11 at 00:27
  • If nobody else comes with a better solution I'm going to accept Don's answer. Still curious why *exactly* did not worked translation of bang pattern with a case expression as described in GHC manual ?? – David Unric Jun 27 '11 at 08:55
5

The essential thing you want the strict zipWith to do is to evaluate the elements of the list at the time the cons cell is forced. Your function does not do that. Just try

main = print $ length $ zipWith' undefined [1..10] [1..100]

It's a fluke of the strictness analysis when you use (+) that makes it work.

The function you want is something like this:

zipW f (x:xs) (y:ys) = let z = f x y in seq z (z : zipW f xs ys)
zipW _ _ _ = []

You cannot decouple the production of the cons cell from the production of the value, because the former should force the latter.

augustss
  • 22,884
  • 5
  • 56
  • 93
  • Although difficult to expect, it sounds logical about the need of prior strictness applying at list construction. But the original question remains unanswered: is it possible in list comprehension ? Ie. is there more concise way then my zipWith' f l1 l2 = [ f e1 e2 | (e1,e2) <- zip (seqLst l1) (seqLst l2) ] where seqLst (x:xs) = (x `seq` x : seqLst xs) seqLst _ = [] – David Unric Jun 27 '11 at 00:17
3

To explain the original poster's example and some of Don's noted behavior. Using (>>=) as a concise concatMap, the original example translates into:

zip l1 l2 >>= \(!e1, !e2) -> [f e1 e2]

The second example is:

zip l1 l2 >>= \(e1, e2) -> [e1 `seq` e2 `seq` f e1 e2]

But what the desugared first version actually further desugars into is:

zip l1 l2 >>= \(e1, e2) -> e1 `seq` e2 `seq` [f e1 e2]

e1 and e2 get forced when the concat in concatMap flattens these singleton lists which provides the "force as I go along" behavior.

While this isn't what one would normally think of as a "strict" zipWith, it is not a fluke that it works for the fibs example.

  • You're right, it's not exactly a fluke since `zipWith'` gets `fibs` as an argument. It will just force the elements one "step" later than a real strip `zipWith` would. It is a fluke of this example. :) – augustss Jun 27 '11 at 06:37