As a rule, code in the where
or let
block of a constant applicative form is evaluated only once, and only as deep as necessary (i.e., if it's not used at all it also won't be evaluated at all).
f
is not a constant applicative form because it has arguments; it's equivalent to
f' = \a b -> let e1 = <lengthy computation>
in if a==b
then <simple computation>
else e1 + 2 * e1 - e1^2
So, e1
is evaluated once every time you call the function with both arguments. This is likely also what you want, and in fact the best behaviour possible if <lengthy computation>
depends on both a
and b
. If it, say, only depends on a
, you can do better:
f₂ a = \b ->
if a==b then <simple computation>
else e1 + 2 * e1 - e1^2
where e1 = <lengthy computation>
This form will be more efficient when you do e.g. map (f 34) [1,3,9,2,9]
: in that example, e1
would only be computed once for the entire list. (But <lengthy computation>
won't have b
in scope, so it can't depend on it.)
OTOH, there can also be scenarios where you don't want e1
to be kept at all. (E.g. if it occupies a lot of memory, but is rather quick to compute). In this case, you can just make it a “nullary function”
f₃ a b
| a==b = <simple computation>
| otherwise = e1() + 2 * e1() - e1()^2
where e1 () = <lengthy computation>
Functions are not memoized by default, so in the above, <lengthy computation>
is done zero times if a==b
and three times else.
Yet another possibility is to force that e1
is always evaluated exactly once. You can do that with seq
:
f₄ a b = e1 `seq` if a==b
then <simple computation>
else e1 + 2 * e1 - e1^2
where e1 = <lengthy computation>
This is the only of the suggestions that actually changes something about the semantics, not just the performance: assume we define always e1 = error "too tough"
. Then f
, f'
, f₂
and f₃
will all still work provided that a==b
; however f₄
will even fail in that case.
As for optimisations (-O
or -O2
) – these generally won't change anything about the strictness properties of your program (i.e. can't change between the behaviour of f
and f₄
). Beyond that, the compiler is pretty much free to make any change it considers benefitial to performance. But usually, it will not change anything about what I said above. The main exception, as Taren remarks, is something like f₃
: the compiler will readily inline e1 ()
and then share a reference to the computed value, which prevents the garbage collector from reclaiming the memory. So it's better not to rely on this (anyway somewhat hackish) technique.