3

For example, say this is my function:

let fib = n => {
  switch (n) {
    case 0: return 0
    case 1: return 1
    default: return fib(n - 1) + fib(n - 2)
  }
}

I can then implement a basic memoize function...

let memoize = fn => {
  let cache = new Map   // mutates!
  return _ => {
    if (!cache.has(_)) {
      cache.set(_, fn(_))
    }
    return cache.get(_)
  }
}

... And use it to implement a memoized fib:

let fib2 = memoize(n => {
  switch (n) {
    case 0: return 0
    case 1: return 1
    default: return fib2(n - 1) + fib2(n - 2)
  }
})

However, that memoize function uses mutable state internally. I can reimplement memoize as a monad, so every time we call fib it returns a tuple of [value, fib] where fib now has something in cache, leaving the original fib unmodified.

The downside of the monad approach is it is not idiomatic JavaScript, and is hard to use without a static type system.

Is there some other way to implement a memoize function that avoids mutation, without resorting to monads?

bcherny
  • 3,072
  • 2
  • 27
  • 35
  • Considering the given answer it would be possible provided Javascript had a non-strict evaluation strategy –  Jun 03 '17 at 11:23

1 Answers1

2

First, even in Haskell, which is "pure" by all reasonable standards, thunks are internally overwritten by their result after evaluation -- read about graph reduction. This is a tradeoff between purity and efficiency, which however hides the impurity from the user, and thus makes it an implementation detail.

But the answer to your question is yes. Consider what you'd do in a purely imperative setting: dynamic programming. You'd think about how you can construct the function as a table lookup, and then build that table from bottom up. Now, what many people applying this is that this just is building a memoized recursive function.

You can turn the principle around, and in a functional language use the "bottom-up table trick" to get a memoized recursive function, just by building the lookup table in a pure manner:

fib n = fibs !! n
  where fibs = 0:1:zipWith (+) fibs (tail fibs)

Translated to a lazy pseudo-JS:

let fibs = [0, 1, Array.zipWith((a, b) => a + b, fibs, fibs.tail)...]
let fib = (n) => fibs[n]

In Haskell this works by default, since it is lazy. In JavaScript, you can probably do something similar by using lazy streams. Compare the following Scala variant:

def fibonacci(n: Int): Stream[Int] = {
   lazy val fib: Stream[Int] = 0 #:: fib.scan(1)(_+_)
   fib.take(n)
}

(scan is like reduce, but keeps intermediate accumulations; #:: is cons for streams; and a lazy val value is essentially a memoizing thunk.)

For further pondering about the implementation of fib in the presence of graph reduction, see How is this fibonacci-function memoized?.

phipsgabler
  • 20,535
  • 4
  • 40
  • 60
  • Ah, I wondered the whole time how Haskell achieves sharing of results of evaluated thunks. Now, this feels like cheating. –  Jun 02 '17 at 17:41
  • Well, it's a really elegant separation. You have your pure graphs, and then can compile them to an [abstract machine](https://ai2-s2-pdfs.s3.amazonaws.com/297e/4586ec4ebcd5a26ab713af9ae95d2d0763f6.pdf) made for evaluating them -- mutable, but a kind of "assembly language of ML languages". And it's also more subtle than just "overwrite the result". I'm by far not an expert. – phipsgabler Jun 02 '17 at 17:52
  • This makes sense, but in JS is similar to the hashmap-building approach in my example, because the array is mutable. Node's built-in Streams class can only get the latest element of a stream, so we must keep track of already-seen elements ourselves. It sounds like the point of your approach is that it's still mutable, but that mutability isn't exposed to the programmer; because JS doesn't have lazy eval, we need to keep track of the state in the app layer. – bcherny Jun 03 '17 at 17:06
  • @bcherny I think I'm using "stream" in a different sense you -- essentially, a lazy, possibly infinite list, which can have a recursive definition. Such a thing can be implemented an eager language, using closures. See my Scala example -- although thinking about how `lazy val`s are implemented, you're kind of right with your last sentence. – phipsgabler Jun 04 '17 at 08:38
  • FWIW, here's a first pass at a lazy streams implementation in JS - https://gist.github.com/bcherny/2dbb809311a66ea4237846c4f6038d1a – bcherny Jun 06 '17 at 19:46
  • 1
    Now published on NPM as "lazy-arr" - https://www.npmjs.com/package/lazy-arr – bcherny Jun 07 '17 at 15:21
  • That escalated quickly ;) Accidentially, a few minutes ago I found a library which seems to have implement the kind of type I was thinking about: [immutable.js](https://github.com/facebook/immutable-js#lazy-seq) with its [`Seq`](https://facebook.github.io/immutable-js/docs/#/Seq) type. – phipsgabler Jun 07 '17 at 15:33