APL Notes: Memoization

Memoization

Consider this famously slow implementation of the Fibonacci function, in J:

fib =: monad define"0 M.
  if. y = 0 do.
    1
  elseif. y = 1 do.
    1
  elseif. do.
    (fib y - 1) + fib y - 2
  end.
)
echo fib 40

Without the M. that auto-memoizes the function, the last line takes about a minute. The BQN equivalent - without memoization - only takes 13s on CBQN, but that's still very slow compared to the near-instant memoized call.

BQN doesn't have M. but it does have lexical scope, mutable hashmaps, and 1-modifiers:

_memo ← {f _𝕣: m←‒HashMap˜⟨⟩ β‹„ {m.Has π•¨β‹ˆπ•©? m.Get π•¨β‹ˆπ•©; (π•¨β‹ˆπ•©)⊸m.Set⊸⊒ 𝕨F𝕩}}
Fib ← {
  0: 1;
  1: 1;
  π•Š: (Fib 𝕩 - 1) + Fib 𝕩 - 2
}βš‡0 _memo
β€’Show Fib 40

Some notes:

  1. Direct recursion ($: in J, π•Š in BQN) bypasses the cache in both languages.
  2. This one-line feature comes with many advantages over J's primitive, as you can control the cache, clear it, prevent it from getting too large, serialize it to disk, use a derived key rather than the exact arguments - and can memoize more than just atoms.
  3. Manual memoization is also possible in J - Block Jank has an example of closure-like J.

Lil doesn't have these late-bound recursive calls, so a separate memofib doesn't seem possible. Dicts also aren't exactly mutable hashtables, but still work well enough for this example:

m:() on memofib x do
  if !m[x] m[x]:
    if x~0 1 elseif x~1 1 else
      memofib[x-1]+memofib[x-2]
    end
  end
  m[x]
end