3

Consider the program:

l = [0..10]
l' = map (+1) [0..10]

Running it with GHCi, and typing :sprint l and :sprint l' will reveal both lists to be unevaluated. However, after running length l and length l' and then again using sprint:

l = [0,1,2,3,4,5,6,7,8,9,10]

and

l' = [_,_,_,_,_,_,_,_,_,_,_]

I've made similar experiments and tried binding variables to lists in GHCi with let, however only in the case of l (defined as above in a program top-level) is the list always completely evaluated.

These behaviours all point to an optimisation feature, however I was wondering if there is a more elaborate answer (strategy) 'under-the-hood'.

duplode
  • 33,731
  • 7
  • 79
  • 150
Matei
  • 152
  • 1
  • 5
  • Well the *elements* are never evaluated, since we do not need them, and do not make any comparison, equality check with the elements. This has not much to do with a list, it will happen with all datastructures. – Willem Van Onsem Apr 13 '20 at 13:43
  • 1
    Side note: one potential source of confusion can be eliminated by making the lists monomorphic (`[0 :: Integer ..10]`). That way loading the definitions from a file should have the same effect as entering them at the GHCi prompt. – duplode Apr 13 '20 at 14:34
  • I don't see an actual question here. – amalloy Apr 13 '20 at 16:30

1 Answers1

2

The elements of the original [0..10] lists were evaluated in both cases. What was left unevaluated in the l' case were the results of applying (+1) to the list elements. In contrast, here is what happens if we map the function strictly:

GHCi> import Control.Monad
GHCi> l'' = (+1) <$!> [0 :: Integer ..10]
GHCi> :sprint l''
l'' = _
GHCi> length l''
11
GHCi> :sprint l''
l'' = [1,2,3,4,5,6,7,8,9,10,11]

(Note that I am specialising the integer literals, so that the absence of the monomorphism restriction in the GHCi prompt doesn't lead to different results from what you get upon loading the code from a file.)

It is worth noting that enumFromTo for Integer (which is what using the range boils down to), as implemented by base, evaluates the elements in order to know when to stop generating them. That's to say it is not length that forces the list elements, as we'd hope from looking at its definition:

length                  :: [a] -> Int
length xs               = lenAcc xs 0

lenAcc          :: [a] -> Int -> Int
lenAcc []     n = n
lenAcc (_:ys) n = lenAcc ys (n+1) 

To get a better feeling for how length behaves here, we might try repeating your experiment with a list generated by using replicate (which, like length, doesn't look at the elements) on a not fully evaluated value:

GHCi> n = 2 * (7 :: Integer)  -- let-bindings are lazy.
GHCi> :sprint n
n = _
GHCi> l''' = replicate 3 n
GHCi> :sprint l'''
l''' = _
GHCi> length l'''
3
GHCi> :sprint l'''
l''' = [_,_,_]
duplode
  • 33,731
  • 7
  • 79
  • 150
  • 1
    I partly disagree with "`enumFromTo` must evaluate the elements". For instance, `eft a b = take (b-a+1) $ iterate succ a` seems a correct implementation to me for `Int`, and produces only `[1,_,_,_,_,_,_,_,_,_]` after the length is forced. A more contrived example: the "pessimized" `eft a b = map id (eunmFromTo a b)` would also be correct (IMO) and produces unevaluated elements. It's silly, but not impossible to write. – chi Apr 13 '20 at 16:37
  • @chi You're right, of course; the answer has been corrected. I probably shouldn't have tried to get away without mentioning the actual implementation of `enumFromTo @Integer`... – duplode Apr 13 '20 at 16:49