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
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.
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.
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.
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
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.