How should one reason about function evaluation in examples like the following in Haskell:
let f x = ...
x = ...
in map (g (f x)) xs
In GHC, sometimes (f x) is evaluated only once, and sometimes once for each element in xs, depending on what exactly f and g are. This can be important when f x is an expensive computation. It has just tripped a Haskell beginner I was helping and I didn’t know what to tell him other than that it is up to the compiler. Is there a better story?
Update
In the following example (f x) will be evaluated 4 times:
let f x = trace "!" $ zip x x
x = "abc"
in map (\i -> lookup i (f x)) "abcd"
With language extensions, we can create situations where
f xmust be evaluated repeatedly:The crux is to have
f xpolymorphic even as the argument ofg, and we must create a situation where the type(s) at which it is needed can’t be predicted (my first stab used anEither a binstead ofBI, but when optimising, that of course led to only two evaluations off xat most).A polymorphic expression must be evaluated at least once for each type it is used at. That’s one reason for the monomorphism restriction. However, when the range of types it can be needed at is restricted, it is possible to memoise the values at each type, and in some circumstances GHC does that (needs optimising, and I expect the number of types involved mustn’t be too large). Here we confront it with what is basically an inhomogeneous list, so in each invocation of
g (f x), it can be needed at an arbitrary type satisfying the constraints, so the computation cannot be lifted outside themap(technically, the compiler could still build a cache of the values at each used type, so it would be evaluated only once per type, but GHC doesn’t, in all likelihood it wouldn’t be worth the trouble).Bottom line: Always compile with optimisations, help the compiler by binding expressions you want shared to a name, and give monomorphic type signatures where possible.