F# tips weekly #15: Recursive memoize
Memoization of a recursive function can be often useful, but has some pitfalls compared to memoization of a simple function. Let's take a detailed look.
Standard Memoization
Let's use the classical example of computing the n-th item in the Fibonacci sequence. Utilizing the memoize function from Tip #14 works, but we encounter a compiler warning:
let rec fib = memoize <| fun n ->
if n < 2 then
1
else
fib (n - 1) + fib (n - 2)
The warning reads:
This and other recursive references to the object(s) being defined will be checked for initialization-soundness at runtime through the use of a delayed reference. This is because you are defining one or more recursive objects, rather than recursive functions. This warning may be suppressed by using '#nowarn "40"' or '--nowarn:40'.
The issue here is that fib
actually represents a constant object representing the closure of our memoized function. While calling this closure recursively is possible, it can lead to this warning and associated runtime checks.
Recursive Call Placeholder Trick
We can eliminate the warning by employing the following trick. Instead of using the rec
definition, we add a placeholder parameter within the function passed to memoizeRec
:
let fib = memoizeRec <| fun recF n ->
if n < 2 then
1
else
recF (n - 1) + recF (n - 2)
memoizeRec
is defined as:
let memoizeRec f =
let cache = System.Collections.Concurrent.ConcurrentDictionary()
let rec recF x =
cache.GetOrAdd(x, lazy f recF x).Value
recF
Notice that the recursive function is defined inside the memoizeRec
function. This approach allows us to use the rec
keyword only there and not in memoized function, and avoids recursive calls on the object.
Y-Combinator
When we apply the same idea outside the context of memoization, we got a way to define a recursive function without explicitly define it as recursive:
let mkRec f =
let rec recF x = f recF x
recF
We can apply this technique to create recursive functions in the same manner as with memoizeRec
:
let fib =
mkRec <| fun recF n ->
if n < 2 then
1
else
recF (n - 1) + recF (n - 2)
mkRec
is essentially the same thing as the fixed-point combinator or Y-combinator, nicely explained for example here.
Infinite Recursion
When working with recursive functions, we should always be aware of the possibility of infinite recursion. Typically, this leads to a Stack overflow
error. However, in our case, we encounter a different error:
System.InvalidOperationException: ValueFactory attempted to access the Value property of this instance.
This error occurs because we are using a Lazy
object to compute the result, and there is a check to prevent accessing the same instance of Lazy
within its evaluation, indicating a circular reference. While this incurs some performance cost, it comes with the advantage that we catch the problem of infinite recursion earlier, before a stack overflow occurs.
Subscribe to my newsletter
Read articles from Jindřich Ivánek directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Jindřich Ivánek
Jindřich Ivánek
Software developer focused mainly on F#, with a passion for functional programming, problem solving, efficient algorithms and programming languages. I have a strong computer science background, focused mainly in graph theory and graph algorithms; also was intrigued by data structures, complexity, and computability theory. Experienced with backend F# programming. I worked with mathematical optimization (mixed integer programming) and use of statistical methods as well. I contributed to several open source F# projects, mostly around developer tooling.