defmodule Memoize do
@moduledoc """
Module documentation for Memoize.
"""
defmacro __using__(_) do
quote do
import Memoize,
only: [defmemo: 1, defmemo: 2, defmemo: 3, defmemop: 1, defmemop: 2, defmemop: 3]
@memoize_memodefs []
@memoize_origdefined %{}
@before_compile Memoize
end
end
@doc ~S"""
Define the memoized function.
Below code:
defmemo foo(0, y) do
y
end
defmemo foo(x, y) when x == 1 do
y * z
end
defmemo foo(x, y, z \\ 0) when x == 2 do
y * z
end
is converted to:
def foo(t1, t2) do
Memoize.Cache.get_or_run({__MODULE__, :foo, [t1, t2]}, fn -> __foo_memoize(t1, t2) end)
end
def foo(t1, t2, t3) do
Memoize.Cache.get_or_run({__MODULE__, :foo, [t1, t2, t3]}, fn -> __foo_memoize(t1, t2, t3) end)
end
def __foo_memoize(0, y) do
y
end
def __foo_memoize(x, y) when x == 1 do
y * z
end
def __foo_memoize(x, y, z \\ 0) when x == 2 do
y * z
end
"""
defmacro defmemo(call, expr_or_opts \\ nil) do
{opts, expr} = resolve_expr_or_opts(expr_or_opts)
define(:def, call, opts, expr)
end
defmacro defmemop(call, expr_or_opts \\ nil) do
{opts, expr} = resolve_expr_or_opts(expr_or_opts)
define(:defp, call, opts, expr)
end
defmacro defmemo(call, opts, expr) do
define(:def, call, opts, expr)
end
defmacro defmemop(call, opts, expr) do
define(:defp, call, opts, expr)
end
defp resolve_expr_or_opts(expr_or_opts) do
cond do
expr_or_opts == nil ->
{[], nil}
# expr_or_opts is expr
Keyword.has_key?(expr_or_opts, :do) ->
{[], expr_or_opts}
# expr_or_opts is opts
true ->
{expr_or_opts, nil}
end
end
defp define(method, call, _opts, nil) do
# declare function
quote do
case unquote(method) do
:def -> def unquote(call)
:defp -> defp unquote(call)
end
end
end
defp define(method, call, opts, expr) do
register_memodef =
case call do
{:when, meta, [{origname, exprmeta, args}, right]} ->
quote bind_quoted: [
expr: Macro.escape(expr, unquote: true),
origname: Macro.escape(origname, unquote: true),
exprmeta: Macro.escape(exprmeta, unquote: true),
args: Macro.escape(args, unquote: true),
meta: Macro.escape(meta, unquote: true),
right: Macro.escape(right, unquote: true)
] do
require Memoize
fun = {:when, meta, [{Memoize.__memoname__(origname), exprmeta, args}, right]}
@memoize_memodefs [{fun, expr} | @memoize_memodefs]
end
{origname, exprmeta, args} ->
quote bind_quoted: [
expr: Macro.escape(expr, unquote: true),
origname: Macro.escape(origname, unquote: true),
exprmeta: Macro.escape(exprmeta, unquote: true),
args: Macro.escape(args, unquote: true)
] do
require Memoize
fun = {Memoize.__memoname__(origname), exprmeta, args}
@memoize_memodefs [{fun, expr} | @memoize_memodefs]
end
end
fun =
case call do
{:when, _, [fun, _]} -> fun
fun -> fun
end
deffun =
quote bind_quoted: [
fun: Macro.escape(fun, unquote: true),
method: Macro.escape(method, unquote: true),
opts: Macro.escape(opts, unquote: true)
] do
{origname, from, to} = Memoize.__expand_default_args__(fun)
memoname = Memoize.__memoname__(origname)
for n <- from..to do
args = Memoize.__make_args__(n)
unless Map.has_key?(@memoize_origdefined, {origname, n}) do
@memoize_origdefined Map.put(@memoize_origdefined, {origname, n}, true)
case method do
:def ->
def unquote(origname)(unquote_splicing(args)) do
Memoize.Cache.get_or_run(
{__MODULE__, unquote(origname), [unquote_splicing(args)]},
fn -> unquote(memoname)(unquote_splicing(args)) end,
unquote(opts)
)
end
:defp ->
defp unquote(origname)(unquote_splicing(args)) do
Memoize.Cache.get_or_run(
{__MODULE__, unquote(origname), [unquote_splicing(args)]},
fn -> unquote(memoname)(unquote_splicing(args)) end,
unquote(opts)
)
end
end
end
end
end
[register_memodef, deffun]
end
# {:foo, 1, 3} == __expand_default_args__(quote(do: foo(x, y \\ 10, z \\ 20)))
def __expand_default_args__(fun) do
{name, args} = Macro.decompose_call(fun)
is_default_arg = fn
{:\\, _, _} -> true
_ -> false
end
min_args = Enum.reject(args, is_default_arg)
{name, length(min_args), length(args)}
end
# [] == __make_args__(0)
# [{:t1, [], Elixir}, {:t2, [], Elixir}] == __make_args__(2)
def __make_args__(0) do
[]
end
def __make_args__(n) do
for v <- 1..n do
{:"t#{v}", [], Elixir}
end
end
def __memoname__(origname), do: :"__#{origname}_memoize"
defmacro __before_compile__(_) do
quote do
@memoize_memodefs
|> Enum.reverse()
|> Enum.map(fn {memocall, expr} ->
Code.eval_quoted({:defp, [], [memocall, expr]}, [], __ENV__)
end)
end
end
def invalidate() do
Memoize.Cache.invalidate()
end
def invalidate(module) do
Memoize.Cache.invalidate({module, :_, :_})
end
def invalidate(module, function) do
Memoize.Cache.invalidate({module, function, :_})
end
def invalidate(module, function, arguments) do
Memoize.Cache.invalidate({module, function, arguments})
end
defdelegate garbage_collect(), to: Memoize.Cache
def cache_strategy() do
Memoize.Config.cache_strategy()
end
end