In the current exercise assignment of the Functional Programming course I'm doing, we've got to make a memoized version of a given function. To explain memoization, the following example is given:
fiblist = [ fibm x | x <- [0..]]
fibm 0 = 0
fibm 1 = 1
fibm n = fiblist !! (n-1) + fiblist !! (n-2)
But I don't fully understand how this works.
Let's call fibm 3
.
fibm 3
--> fiblist !! 2 + fibList 1
--> [fibm 0, fibm 1, fibm 2] !! 2 + [fibm 0, fibm 1] !! 1
--> fibm 2 + fibm 1
--> (fiblist !! 1 + fiblist 0) + 1
--> ([fibm 0, fibm 1] !! 1 + [fibm 0] !! 0) + 1
--> (fibm 1 + fibm 0) + 1
--> 1 + 0 + 1
--> 2
From other questions/answers and googling I learned that somehow, the evaluated fiblist is shared between calls.
Does this mean that, for example, for fiblist !! 2 + fiblist !! 1
, the list values are only calculated once for fiblist !! 2
and then just reused for fiblist !! 1
?
Then the two fibonacci numbers are calculated only once per call, so no exponential number of calls. But what about the "lower" levels of the call in the fiblist
function? How do they benefit from the calculated fiblist
in the original fibm
call?
The crucial part here is that the list is lazily evaluated, which means that the element isn't computed until the first time it's requested. Once it has been evaluated, however, it's there for anything else to look up. So in your example, you're right in saying that the values are only calculated once for the fiblist !! 2
and then reused for fiblist !! 1
.
The 'lower levels' of the fiblist function work in the same way. The first time I call fiblist !! 1
it will be evaluated by calling fibm 1
, which is just 1, and this value will then remain in the list. When you try to get up higher Fibonacci number, fiblist
will call fibm
which will look up those values in lower - and potentially already evaluated - positions of fiblist
.
Let's walk through the evaluation step by step. In addition to showing the current expression, we show the current state of evaluation of fiblist
in memory. There, I write <expr>
to denote an unevaluated expression (often called a thunk), and >expr<
to denote an unevaluated expression that is currently under evaluation. You can see lazy evaluation in action. The list gets evaluated only as far as demanded, and subcomputations that complete will be shared for future reuse.
Current expression Current evaluation state of fiblist
fibm 3 <[ fibm x | x <- [0..] ]>
-> (simple expansion of the definition)
fiblist !! (3-1) + fiblist !! (3-2) <[ fibm x | x <- [0..] ]>
-> ((+) has to evaluate both its arguments to make progress, let's assume
it starts with the left argument; (!!) traverses the list up to the given
element and returns the element it finds)
fibm 2 + fiblist !! (3-2) <fibm 0> : <fibm 1> : >fibm 2< : <[ fibm x | x <- [3..] ]>
-> (simple expansion of the definition)
(fiblist !! (2-1) + fiblist !! (2-2)) + fiblist !! (3-2)
<fibm 0> : <fibm 1> : >fibm 2< : <[ fibm x | x <- [3..] ]>
-> (we again start with the first argument to (+),
computing the result of (!!) does not cause any
further evaluation of fiblist)
(fibm 1 + fiblist !! (2-2)) + fiblist !! (3-2)
<fibm 0> : >fibm 1< : >fibm 2< : <[ fibm x | x <- [3..] ]>
-> (expanding fibm 1 returns a result immediately;
this concludes the computation of fibm 1,
and the thunk is updated with the result)
(1 + fiblist !! (2-2)) + fiblist !! (3-2)
<fibm 0> : 1 : >fibm 2< : <[ fibm x | x <- [3..] ]>
-> (now we compute fiblist !! (2-2))
(1 + fibm 0) + fiblist !! (3-2) >fibm 0< : 1 : >fibm 2< : <[ fibm x | x <- [3..] ]>
-> (expanding fibm 0 returns 0 immediately, and the
corresponding thunk can be updated)
(1 + 0) + fiblist !! (3-2) 0 : 1 : >fibm 2< : <[fibm x | x <- [3..] ]>
-> (we can compute the (+), yielding the result of
fibm 2; the corresponding thunk is updated)
1 + fiblist !! (3-2) 0 : 1 : 1 : <[fibm x | x <- [3..] ]>
-> (now the right argument of (+) has to be evaluated, but (!!)
will return the already evaluated list element directly)
1 + 1 0 : 1 : 1 : <[fibm x | x <- [3..] ]>
-> (arithmetic; note that as we called fibm 3 directly in the
beginning, instead of fiblist !! 3, the list is unaffected
by this final result)
2 0 : 1 : 1 : <[fibm x | x <- [3..] ]>
As fiblist
is a global constant (often called a CAF for "constant applicative form"), the partially evaluated state of the list will persist and future calls to fibm
will reuse already evaluated elements of the list. The list will eventually grow larger and larger, consuming more and more memory, though.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With