memoizer v0.1.0 Memoizer

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:

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

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

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

Here’s a simple example how to us 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:

defmemo 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 gueards, and it’s legal to mix simple defs with defmemo definitions. This enables you to have a fine grained controll 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, nothings stops you 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.

Link to this section Summary

Functions

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

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

Link to this section Functions

Link to this function compile(module, opts \\ [])

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

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.

Link to this macro defmemo(arg, list) (macro)
Link to this macro defmemop(arg, list) (macro)

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

Link to this function forget(module, function)

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

Link to this function 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.

Link to this function 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
Link to this function 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.