haskellmemoization

Use of !! in memoization


When looking up memoization in Haskell, I found this bit of code:

memoized_fib :: Int -> Integer
memoized_fib = (map fib [0 ..] !!)
   where fib 0 = 0
         fib 1 = 1
         fib n = memoized_fib (n-2) + memoized_fib (n-1)

I'm trying to make sense of why this code memoizes, but I don't understand the use of !! in the second line. The only use I know of !! is as an operator to retrieve an element in a list by index, and I haven't been able to find anything else.


Solution

  • It might be clearer what’s going on if we add a few names and lift things out of the where clause.

    memoized_fib :: Int -> Integer
    memoized_fib i = fib_table !! i
    
    fib_table :: [Integer]
    fib_table = map fib [0 ..]
    
    fib :: Int -> Integer
    fib 0 = 0
    fib 1 = 1
    fib n = memoized_fib (n-2) + memoized_fib (n-1)
    

    map fib [0 ..] makes a list [fib 0, fib 1, fib 2, …] that serves as our memoisation table. memoized_fib is a closure that contains a reference to this table. Given an index argument the closure uses !! to look up a result by index in the table, which will force that result to be computed — supposing you use it for something, like printing it out.

    The important thing is that the table can be shared, because both fib_table and fib are independent of the choice of index i. So the helper definitions don’t need to be in a where clause, that’s just to keep memoized_fib self-contained — and in fact, to guarantee memoisation, i needs to be outside the scope of the helpers, so they can’t depend on i.

    non_memoized_fib i = fib_table !! i
      where
        fib_table = map fib [0 ..]
    -- =
    non_memoized_fib =
      \i -> let
        fib_table = map fib [0 ..]        
      in fib_table !! i
    
    
    memoized_fib = \i -> fib_table !! i
      where
        fib_table = map fib [0 ..]
    -- =
    memoized_fib = (fib_table !!)
      where
        fib_table = map fib [0 ..]
    

    Since the table is a lazy list, it will be computed on demand. For a result that hasn’t been computed yet, fib will call memoized_fib so that it can reuse the saved results for any subcomputations that have already been done.

    If fib just called itself directly, fib_table would still hold a memoised result for any specific index you computed, but each one would be computed separately: it wouldn’t share any of the work with lower indices.