Function memoization goes back at least as far as Donald Michie’s 1968 paper. The idea is to stash away results of a function call for reuse when a function is called more than once with the same argument. Given an argument, the memoized function looks up the argument in the internal memo table (often a hash table). If found, the previously computed result is reused. Otherwise, the function is applied, and the result is stored in the table, keyed by the argument. The table and its mutation are kept private from clients of the memo function.

Perhaps surprisingly, memoization can be implemented simply and purely functionally in a lazy functional language. Laziness allows the implementation to build the memo table once and for all, filling in all the results for the function at all domain values. Thanks to laziness, the values don’t actually get computed until they’re used. As usual with lazy data structures, once a component has been evaluated, future accesses come for free.

The implementation described in this post is based one I got from Spencer Janssen. It uses the essential idea of Ralf Hinze’s paper Generalizing Generalized Tries. The library is available on Hackage and a darcs repository. See the wiki page and hackage page for documentation and source code. I’ve compiled it successfully with ghc versions 6.8.2 through 6.10.3.

Memo tries

The central idea of the function memoizer is associating a type of trie to each domain type want to memoize over.

classHasTrie a wheredata (↛) a ∷* → *

where a ↛ b represents a trie that maps values of type a to values of type b. The trie representation depends only on a. For more information on the recent "associated types" feature of Haskell, see Associated Types with Class.

In addition to the trie type, the HasTrie class contains converters between functions and tries:

trie ∷ (a → b) → (a ↛ b) untrie ∷ (a ↛ b) → (a → b)

The trie and untrie methods must be inverses:

trie ∘ untrie ≡ iduntrie ∘ trie ≡ id

Given the HasTrie class, memoization is trivial to implement:

memo ∷HasTrie a ⇒ (a → b) → (a → b)memo = untrie ∘ trie

The second inverse property implies that memo is the identity function (semantically).

Multi-argument curried functions can be memoized by repeated uses of memo. See the source code.

(1) So far, I don’t believe associated types are necessary. HasMemo should be implementable with the simple interface:

class HasMemo a where
memo :: (a -> r) -> (a -> r)

All the compositions you have used, and every other composition I have been able to think of, are implementable with this structure.

(2) This one is more subjective, but I don’t think the typeclass approach to memoization is a good one. Here you are limited to one memo trie per type. But there are many ways to memoize a given type! Indeed, you could use newtypes, but that’s clunky and a lot of work. data-memocombinators, which I just released on hackage two days ago (uh… are you and I the same person? :-), exposes memoizers as first-class functions so that they can be custom built; i.e. so you can memoize only integers less than 1,000,000 (the best way to solve one of the Euler problems), or only memoize the Left side of an Either (let’s say the right doesn’t have a trie), etc.

I think competition on Hackage will be nice, to see which approach ends up being more useful. The two approaches can be trivially combined by picking a default memoizer for each type. I wonder if there is a deeper, more flexible way to combine them.

I am not quite sure in what essential way the strictness is changed. For example, it is theoretically possible to memoize a function of infinite streams, given that the return type is compact, so it’s not just that it makes it fully strict in its argument’s domain. I haven’t gotten around to implementing this awesome possibility just yet

[…] I don’t think the typeclass approach to memoization is a good one. Here you are limited to one memo trie per type. But there are many ways to memoize a given type!

Yep. A problem with typeclasses in general. Even more generally, a problem with hiding implementation mechanism. Prettier but less flexible. I gather you’ve found the flexibility particularly useful with memoization.

data-memocombinators, which I just released on hackage two days ago […]

Sounds awfully cool. I’d love to read a blog post about your library, especially where you exploit the flexibility of explicit memoizers.

[…] However, there are several other ways to make linear maps, and it would be easy to forget to memoize each combining form. So, instead of the function representation above, I ensure that the function be memoized by representing it as a memo trie. […]

[…] using the essential idea of Ralf Hinze’s paper Generalizing Generalized Tries. The blog post Elegant memoization with functional memo tries describes a library, MemoTrie, based on both of these sources, and using associated data types. I […]

That is, there are no functions Void -> a, and a^Void is isomorphic to Unit. However, this instance makes constant functions strict, so that
memo (const a) undefined == undefined.
This can be alleviated with an instance closer to the Unit instance.

Any thoughts? Would you be willing to add a Void instance to MemoTrie?