2

I am trying to understand the lazy evaluation of Haskell and made the following attempt in GHCi

GHCi, version 8.0.2: http://www.haskell.org/ghc/  :? for help
Prelude> :set +m
Prelude> let iterateUntilError1 :: (a->a) -> (a->a)
Prelude|     iterateUntilError1 f = (iterateUntilError1 f) . f
Prelude| 
Prelude> let iterateUntilError2 :: (a->a) -> (a->a)
Prelude|     iterateUntilError2 f = \x -> (iterateUntilError2 f) . f $ x
Prelude| 
Prelude> let iterateUntilError3 :: (a->a) -> (a->a)
Prelude|     iterateUntilError3 f = \x -> (iterateUntilError3 f) . f $! x
Prelude| 
Prelude> let iterateUntilError4 x = foldl1 (.) (repeat (($!) x))
Prelude> iterateUntilError3 tail [1..5]
*** Exception: Prelude.tail: empty list
Prelude> iterate tail [1..5]
[[1,2,3,4,5],[2,3,4,5],[3,4,5],[4,5],[5],[],*** Exception: Prelude.tail: empty list

Among the 4 versions presented, only iterateUntilError3 works as expected, the other 3 versions enter an infinite loop that has to be stopped via Ctrl+C. I don't quite understand why the other three versions don't work in this case.

The related question laziness and function composition (haskell, erlang) does not seem to address the issues presented in this question.

Weijun Zhou
  • 746
  • 1
  • 7
  • 25
  • After various attempts, I found `flip (foldl' (&)) (repeat x)` with `(&)` from `Data.Function` to be a working version with `fold*` and friends. – Weijun Zhou Dec 13 '19 at 17:06
  • 1
    yes, because `z & x = x $ z` and `foldl'` is forcing the accumulator value as it goes along the list, that is equivalent to `flip (foldl' (&)) (repeat x) z = foldl' (&) z (repeat x) = let {z1 = x $! z} in foldl' (&) z1 (repeat x) = let {z1 = x $! z} in let {z2 = x $! z1} in foldl' (&) z2 (repeat x) = ...`. – Will Ness Dec 14 '19 at 08:56

1 Answers1

3

Let's start with iterateUntilError1. If you try to evaluate

iterateUntilError1 tail [1..5] =

expand definition of iterateUntilError1:

((iterateUntilError1 tail) . tail) [1..5] =

expand definition of .:

(iterateUntilError1 tail) (tail [1..5]) =

wait, we have iterateUntilError1 tail again!

((iterateUntilError1 tail) . tail) (tail [1..5]) =
(iterateUntilError1 tail) (tail (tail [1..5]))

and so on.

With iterateUntilError3 you have (inserting some extra parentheses to make priority more clear)

iterateUntilError3 tail [1..5] =
((iterateUntilError3 tail) . tail) $! [1..5] =
((iterateUntilError3 tail) . tail) (1:[2..5]) = 
iterateUntilError3 tail (tail (1:[2..5])) =
(iterateUntilError3 tail . tail) $! (tail (1:[2..5])) =
(iterateUntilError3 tail . tail) (2:[3..5]) = ...

and so it ends up getting an error.

With iterateUntilError2, you have similar evaluation until the line before last where $! forces tail to reduce until getting a constructor and $ doesn't:

(iterateUntilError2 tail . tail) $ (tail [1..5]) =
(iterateUntilError2 tail . tail) (tail [1..5]) =
(iterateUntilError2 tail) (tail (tail [1..5])) = ...

And finally (using tail_ for (tail $!) for simplicity):

iterateUntilError4 tail [1..5] =
foldl1 (.) (repeat tail_) [1..5] = 
foldl (.) tail_ (repeat tail_) [1..5] =
foldl (.) (tail_ . tail_) (repeat tail_) [1..5] =
foldl (.) (tail_ . tail_ . tail_) (repeat tail_) [1..5] = ...

(for this one I didn't follow the actual definition but the idea should still be right, I think).

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • the actual definition is just an implementation of the equation specification above it, so it's all good. – Will Ness Dec 14 '19 at 09:00