Memoizer (memoizer v0.2.1)

Memoizer turns an ordinary Elixir module into a transparent service that memoizes (aka caches) your function calls. The first function call may take a very long time:

:timer.tc(Ultimate, :answer, [:life, :universe, :everything]) #=> {7.5 million years in microseconds, 42}

but further calls happen almost instantaneously when the answer is already available:

:timer.tc(Ultimate, :answer, [:life, :universe, :everything]) #=> {0, 42}

Here's a simple example of how to use it. Let's make the module below a memoized one:

defmodule Memo do
  def fact(0), do: 1
  def fact(n), do: n * fact(n - 1)
end

The modification is simple:

defmodule Memo do
  use Memoizer

  def     fact(0), do: 1
  defmemo fact(n), do: n * fact(n - 1)
end

It is also important that Memo has to be started before you start calculating values. The simplest way is to call Memo.start_link if you are just playing around, but you should consider putting it under supervision.

When the service is running, you can make calls to the functions just as they were defined in the first place: Memo.fact(100). This caches the return values of fact/1 for params between 2 and 100. Next time you call the function within this range, the stored value is returned.

With defmemo you can also use when guards, and it's legal to mix simple defs with defmemo definitions. This enables you to have a fine-grained control over what makes it into the cache and what gets calculated over and over again with every function call. For instance, it does not make much sense to memoize fact(0) in the example above, so we can keep this case in a normal function clause. Here's an example of calculating the Fibonacci sequence with only memoizing fib(n) when n or n-1 can be divided by 7. This reduces the memory footprint to 2/7 of the version memoizing every value:

def     fib(0),                     do: 0
def     fib(1),                     do: 1
def     fib(n) when rem(n, 7) == 2, do:  1 * fib(n - 1) +  1 * fib(n - 2)
def     fib(n) when rem(n, 7) == 3, do:  2 * fib(n - 2) +  1 * fib(n - 3)
def     fib(n) when rem(n, 7) == 4, do:  3 * fib(n - 3) +  2 * fib(n - 4)
def     fib(n) when rem(n, 7) == 5, do:  5 * fib(n - 4) +  3 * fib(n - 5)
def     fib(n) when rem(n, 7) == 6, do:  8 * fib(n - 5) +  5 * fib(n - 6)
defmemo fib(n) when rem(n, 7) == 0, do: 13 * fib(n - 6) +  8 * fib(n - 7)
defmemo fib(n) when rem(n, 7) == 1, do: 21 * fib(n - 7) + 13 * fib(n - 8)

You may want to normalize the arguments of a function call before making the expensive function call, or post-process the returned value before returning it to the caller. In this case you can use defmemop to define your calculation, and wrap the call in a public function that makes the necessary transformations and normalizations. defmemop can be mixed with defp caluses just like defmemo and def.

While memoizing pure functions does not change the semantics of your program, nothing stops you from applying the technique to non-pure functions. This may make sense in certain situations. In this case functions like Memoizer.forget, Memoizer.memoized? and Memoizer.learn can come in handy. It is a deliberate decision that these functions are kept in the Memoizer module and are not injected into the modules that use Memoizer.

If you are after more performance, Memoizer.compile can give you some more speed by compiling the currently cached function clauses into a separate module.

Summary

Functions

Compiles the given Memoizer module into another module where the memoized function calls are defined as separate function clauses.

forget(module) deletes all cached data of all functions under the given module.

forget(module, function) forces Memoizer to delete all cached cases of the function in the module.

Asks Memoizer to forget the return value associated with the function in the module when called with the params.

Stores the return_value of the function in the module module associated with the arguments params.

Checks if the function in the module with the params is memoized.

Functions

compile(module, opts \\ [])

Compiles the given Memoizer module into another module where the memoized function calls are defined as separate function clauses.

defmodule M do
  use Memoizer

  defmemo f(n), do: n + 100
end

M.start_link

M.f 100  # =>  200
M.f 1000 # => 1100

Memoizer.compile M, into: CompileTest

This creates a module called CompileTest as if it were defined as follows:

defmodule CompileTest do
  def f(100),  do: 200
  def f(1000), do: 1100
end

Compilation is a very slow process, especially when there are a huge number of memoized cases. However, the compiled version can be approximately 30% faster than the simple, memoized function calls. You may take advantage of the Erlang runtime allowing you to have two loaded versions of modules loaded at the same time.

defmemo(head, list)

(macro)

defmemop(head, list)

(macro)

forget(module)

forget(module) deletes all cached data of all functions under the given module.

forget(module, function)

forget(module, function) forces Memoizer to delete all cached cases of the function in the module.

forget(module, function, params)

Asks Memoizer to forget the return value associated with the function in the module when called with the params.

Blocks the caller until the cache modification takes effect.

Returns the module in order to make it simple to call Memoizer API functions in a chained manner.

learn(module, function, params, return_value)

Stores the return_value of the function in the module module associated with the arguments params.

Blocks the caller until the cache update takes effect.

If the value was already cached, this call replaces the old value with the new one.

Returns the module name in order to make chaining of Memoizer calls simple.

Example

MyModule
|> Memoizer.learn(:funct, [1], "one")
|> Memoizer.learn(:funct, [2], "two")
|> Memoizer.forget(:funct, [500])
|> Memoizer.memoized?(:funct, [500]) # => false

memoized?(module, function, params)

Checks if the function in the module with the params is memoized.

module has to be a module using the Memoizer. module and function are atoms, params is the list of function arguments. The call returns true or false.