├── .gitignore ├── Gemfile ├── .travis.yml ├── lib ├── defmemo_result_table.ex ├── defmemo_result_table_gs.ex └── defmemo.ex ├── mix.exs ├── Guardfile ├── LICENSE.txt ├── config └── config.exs ├── Gemfile.lock ├── test ├── test_helper.exs ├── defmemo_test.exs └── defmemo_result_table_gs_test.exs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | mix.lock 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | 2 | group :development do 3 | gem 'guard-elixir', path: '~/Elixir/guard-elixir' 4 | end 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: elixir 3 | elixir: 4 | - 1.0.0 5 | - 1.0.1 6 | - 1.0.2 7 | - 1.0.3 8 | otp_release: 9 | - 17.0 10 | - 17.1 11 | - 17.4 12 | sudo: false 13 | notifications: 14 | recipients: 15 | - leej@librely.com 16 | -------------------------------------------------------------------------------- /lib/defmemo_result_table.ex: -------------------------------------------------------------------------------- 1 | defmodule DefMemo.ResultTable do 2 | use Behaviour 3 | 4 | defcallback start_link :: any | nil 5 | defcallback get(fun :: Fun, args :: List) :: any 6 | defcallback put(fun :: Fun, args :: List, result :: any) :: any 7 | defcallback delete(fun :: Fun, args :: List) :: any 8 | end 9 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Memoize.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :defmemo, 6 | version: "0.1.1", 7 | elixir: "~> 1.0", 8 | description: description, 9 | package: package, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [ 15 | mod: {DefMemo, []} 16 | ] 17 | end 18 | 19 | defp deps do 20 | [] 21 | end 22 | 23 | defp description do 24 | """ 25 | A memoization macro (defmemo) for elixir using a genserver backing store. 26 | """ 27 | end 28 | 29 | defp package do 30 | [ 31 | files: ["lib", "priv", "mix.exs", "README*", "readme*", "LICENSE*", "license*"], 32 | contributors: ["Adrian Lee", "(Adapted from work by Gustavo Brunoro)"], 33 | licenses: ["MIT"], 34 | links: %{"GitHub" => "https://github.com/os6sense/DefMemo"} 35 | ] 36 | 37 | end 38 | 39 | 40 | end 41 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | ## Uncomment and set this to only include directories you want to watch 5 | # directories %w(app lib config test spec features) 6 | 7 | ## Uncomment to clear the screen before every task 8 | # clearing :on 9 | 10 | ## Guard internally checks for changes in the Guardfile and exits. 11 | ## If you want Guard to automatically start up again, run guard in a 12 | ## shell loop, e.g.: 13 | ## 14 | ## $ while bundle exec guard; do echo "Restarting Guard..."; done 15 | ## 16 | ## Note: if you are using the `directories` clause above and you are not 17 | ## watching the project directory ('.'), then you will want to move 18 | ## the Guardfile to a watched dir and symlink it back, e.g. 19 | # 20 | # $ mkdir config 21 | # $ mv Guardfile config/ 22 | # $ ln -s config/Guardfile . 23 | # 24 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 25 | 26 | guard :elixir do 27 | watch(%r{^test/(.*)_test\.exs}) 28 | watch(%r{^lib/(.+)\.ex$}) { |m| "test/#{m[1]}_test.exs" } 29 | watch(%r{^test/test_helper.exs$}) { "test" } 30 | end 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2015 Adrian Lee 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a 5 | copy of this software and associated documentation files (the "Software"), to 6 | deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | sell copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, 14 | # level: :info 15 | # 16 | # config :logger, :console, 17 | # format: "$date $time [$level] $metadata$message\n", 18 | # metadata: [:user_id] 19 | 20 | # It is also possible to import configuration files, relative to this 21 | # directory. For example, you can emulate configuration per environment 22 | # by uncommenting the line below and defining dev.exs, test.exs and such. 23 | # Configuration from the imported file will override the ones defined 24 | # here (which is why it is important to import them last). 25 | # 26 | # import_config "#{Mix.env}.exs" 27 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ~/Elixir/guard-elixir 3 | specs: 4 | guard-elixir (0.0.31) 5 | guard 6 | guard-compat (~> 1.0) 7 | 8 | GEM 9 | specs: 10 | celluloid (0.16.0) 11 | timers (~> 4.0.0) 12 | coderay (1.1.0) 13 | ffi (1.9.6) 14 | formatador (0.2.5) 15 | guard (2.12.4) 16 | formatador (>= 0.2.4) 17 | listen (~> 2.7) 18 | lumberjack (~> 1.0) 19 | nenv (~> 0.1) 20 | notiffany (~> 0.0) 21 | pry (>= 0.9.12) 22 | shellany (~> 0.0) 23 | thor (>= 0.18.1) 24 | guard-compat (1.2.1) 25 | hitimes (1.2.2) 26 | listen (2.9.0) 27 | celluloid (>= 0.15.2) 28 | rb-fsevent (>= 0.9.3) 29 | rb-inotify (>= 0.9) 30 | lumberjack (1.0.9) 31 | method_source (0.8.2) 32 | nenv (0.2.0) 33 | notiffany (0.0.6) 34 | nenv (~> 0.1) 35 | shellany (~> 0.0) 36 | pry (0.10.1) 37 | coderay (~> 1.1.0) 38 | method_source (~> 0.8.1) 39 | slop (~> 3.4) 40 | rb-fsevent (0.9.4) 41 | rb-inotify (0.9.5) 42 | ffi (>= 0.5.0) 43 | shellany (0.0.1) 44 | slop (3.6.0) 45 | thor (0.19.1) 46 | timers (4.0.1) 47 | hitimes 48 | 49 | PLATFORMS 50 | ruby 51 | 52 | DEPENDENCIES 53 | guard-elixir! 54 | -------------------------------------------------------------------------------- /lib/defmemo_result_table_gs.ex: -------------------------------------------------------------------------------- 1 | defmodule DefMemo.ResultTable.GS do 2 | @behaviour DefMemo.ResultTable 3 | 4 | @moduledoc """ 5 | GenServer backing store for the results of the function calls. 6 | """ 7 | use GenServer 8 | 9 | def start_link do 10 | GenServer.start_link(__MODULE__, Map.new, name: :result_table) 11 | end 12 | 13 | def get(fun, args) do 14 | GenServer.call(:result_table, { :get, fun, args }) 15 | end 16 | 17 | def delete(fun, args) do 18 | GenServer.call(:result_table, { :delete, fun, args }) 19 | end 20 | 21 | def delete_all do 22 | GenServer.call(:result_table, { :delete_all }) 23 | end 24 | 25 | def put(fun, args, result) do 26 | GenServer.cast(:result_table, { :put, fun, args, result }) 27 | result 28 | end 29 | 30 | def handle_call({ :get, fun, args }, _sender, map) do 31 | reply(Map.fetch(map, { fun, args }), map) 32 | end 33 | 34 | def handle_call({ :delete, fun, args }, _sender, map) do 35 | reply({ :ok, nil }, Map.delete(map, { fun, args })) 36 | end 37 | 38 | def handle_call({ :delete_all }, _sender, _map) do 39 | reply({ :ok, nil }, Map.new) 40 | end 41 | 42 | def handle_cast({ :put, fun, args, result }, map) do 43 | { :noreply, Map.put(map, { fun, args }, result) } 44 | end 45 | 46 | defp reply(:error, map) do 47 | { :reply, { :miss, nil }, map } 48 | end 49 | 50 | defp reply({:ok, val}, map) do 51 | { :reply, { :hit, val }, map } 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | defmodule TimedFunction do 4 | def time(fun) do 5 | {start, return, stop} = {:os.timestamp, fun.(), :os.timestamp} 6 | {return, :timer.now_diff(stop, start) } 7 | end 8 | end 9 | 10 | defmodule Fib do 11 | @doc ~S""" 12 | """ 13 | def fibs(0), do: 0 14 | def fibs(1), do: 1 15 | def fibs(n), do: fibs(n - 1) + fibs(n - 2) 16 | end 17 | 18 | defmodule FibMemo do 19 | import DefMemo 20 | 21 | defmemo fibs(0), do: 0 22 | defmemo fibs(1), do: 1 23 | defmemo fibs(n), do: fibs(n - 1) + fibs(n - 2) 24 | end 25 | 26 | defmodule FibMemoOther do 27 | import DefMemo 28 | 29 | defmemo fibs(0), do: "ZERO" 30 | defmemo fibs(1), do: "A NUMBER ONE" 31 | defmemo fibs(2), do: "A NUMBER TWO!!" 32 | defmemo fibs(n), do: "THE NUMBER #{n} IS BORING" 33 | defmemo fibs(n, x), do: "#{x} AND #{n} /2" 34 | end 35 | 36 | defmodule TestMemoWhen do 37 | import DefMemo 38 | 39 | defmemo fibs(n, x) when is_list(n) and is_binary(x), do: {n, x} 40 | # nb, is binary also covers bitstring 41 | defmemo fibs(n) when is_binary(n), do: {:binary, n} 42 | defmemo fibs(n) when is_boolean(n), do: {:boolean, n} 43 | defmemo fibs(n) when is_atom(n), do: {:atom, n} # nb: atom will match boolean if preceeds it. 44 | defmemo fibs(n) when is_float(n), do: {:float, n} 45 | defmemo fibs(n) when is_list(n), do: {:list, n} 46 | #defmemo fibs(n) when is_integer(n), do: {:integer, n} 47 | defmemo fibs(n) when is_function(n), do: {:function, n} 48 | defmemo fibs(n) when is_map(n), do: {:map, n} 49 | #defmemo fibs(n) when is_number(n), do: {:number, n} 50 | defmemo fibs(n) when is_pid(n), do: {:pid, n} 51 | defmemo fibs(n) when is_port(n), do: {:port, n} 52 | defmemo fibs(n) when is_reference(n), do: {:reference, n} 53 | defmemo fibs(n) when is_tuple(n), do: {:tuple, n} 54 | 55 | defmemo fibs(n), do: {:no_guard, n} 56 | end 57 | 58 | defmodule TestMemoNormalized do 59 | import DefMemo 60 | 61 | defp normalize_case([x]), do: String.downcase(x) 62 | 63 | defmemo slow_upper(s), normalize_case do 64 | :timer.sleep(1) # Could this be why is this code so slow?! 65 | String.upcase(s) 66 | end 67 | 68 | defp normalize_many([numbers, multiply_instead]), do: [Enum.sort(numbers), multiply_instead] 69 | 70 | defmemo slow_sum(n, multiply_instead), normalize_many do 71 | :timer.sleep(1) 72 | if multiply_instead, do: Enum.reduce(n, fn(x, acc) -> x * acc end), else: Enum.sum(n) 73 | end 74 | 75 | end 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DefMemo 2 | ======= 3 | A memoization macro (defmemo) for Elixir. 4 | 5 | [![Build Status](https://travis-ci.org/os6sense/DefMemo.svg?branch=master)](https://travis-ci.org/os6sense/DefMemo) 6 | 7 | Adapted from : (Gustavo Brunoro) https://gist.github.com/brunoro/6159378 8 | 9 | I found Gustavo's Gist when looking at memoization and elixir and fixed it 10 | to work with version 1.0.x. Since then I've fixed a few of the problems with 11 | the original implementation: 12 | 13 | - will correctly memoize the results of functions with identical signatures 14 | but in different modules. 15 | 16 | - will work with 'when' guard clauses in function definitions. (That was fun!) 17 | 18 | - Added lots of lovely tests. 19 | 20 | Usage 21 | ===== 22 | 23 | Add defmemo to your mix.exs file: 24 | 25 | {:defmemo, "~> 0.1.0"} 26 | 27 | And run: 28 | 29 | mix deps.get 30 | 31 | Before *using* a defmemo'd function (it's fine to define them), start_link must 32 | be called. e.g. 33 | 34 | DefMemo.start_link 35 | 36 | or you can add :defmemo into the applications section of your mix.exs: 37 | 38 | [applications: [:logger, :defmemo]] 39 | 40 | Example 41 | ======= 42 | 43 | defmodule FibMemo do 44 | import DefMemo 45 | 46 | defmemo fibs(0), do: 0 47 | defmemo fibs(1), do: 1 48 | defmemo fibs(n), do: fibs(n - 1) + fibs(n - 2) 49 | 50 | def fib_10 do 51 | fibs(10) 52 | end 53 | end 54 | 55 | Performance 56 | =========== 57 | As you would expect for something like fibs, memoization provides dramatic 58 | performance improvements: 59 | 60 | UNMEMOIZED VS MEMOIZED 61 | *********************** 62 | fib (unmemoized) 63 | function -> {result, running time(μs)} 64 | ================================== 65 | fibs(30) -> {832040, 31089} 66 | fibs(30) -> {832040, 31833} 67 | 68 | FibMemo (memoized) 69 | ================================== 70 | fibs(30) -> {832040, 79} 71 | fibs(30) -> {832040, 3} 72 | fibs(50) -> {12586269025, 103} 73 | fibs(50) -> {12586269025, 3} 74 | 75 | Note that these have also improved from version 0.1 to 0.1.1. The above numbers 76 | are on the low end of the spectrum with access ranging from 2 to 15 μs for me. 77 | 78 | TODO 79 | ==== 80 | - Add test for supervisor crashing. 81 | - Look at injecting the type of result table used. 82 | - Better documentation. 83 | - More tests (alwaaaays with the testing!) 84 | - Test with some biger data (e.g. for something like web crawling) 85 | 86 | - ~~Supervisor ~~ 87 | - ~~Redis Based ResultTable - I've been playing with this - obviously there are 88 | limitations on type and it's slower than gen server but there are of course 89 | circumstances where it could be useful but for the most part its not a good 90 | "fit".~~ 91 | 92 | -------------------------------------------------------------------------------- /test/defmemo_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DefMemo.Test do 2 | use ExUnit.Case 3 | 4 | import IO, only: [puts: 1] 5 | 6 | @tag timeout: 100_000 7 | test "The Proof Is In The Pudding" do 8 | puts "\nUNMEMOIZED VS MEMOIZED " 9 | puts "***********************" 10 | puts "fib (unmemoized)" 11 | puts "function -> {result, running time(μs)}" 12 | puts "==================================" 13 | puts "fibs(30) -> #{inspect TimedFunction.time fn -> Fib.fibs(30) end}" 14 | puts "fibs(30) -> #{inspect TimedFunction.time fn -> Fib.fibs(30) end}" 15 | 16 | puts "\nFibMemo (memoized)" 17 | puts "==================================" 18 | puts "fibs(30) -> #{inspect TimedFunction.time fn -> FibMemo.fibs(30) end}" 19 | puts "fibs(30) -> #{inspect TimedFunction.time fn -> FibMemo.fibs(30) end}" 20 | puts "fibs(50) -> #{inspect TimedFunction.time fn -> FibMemo.fibs(50) end}" 21 | puts "fibs(50) -> #{inspect TimedFunction.time fn -> FibMemo.fibs(50) end}" 22 | end 23 | 24 | test "identical function signatures in different modules return correct results" do 25 | FibMemo.fibs(20) 26 | FibMemoOther.fibs(20) 27 | 28 | assert FibMemo.fibs(20) == 6765 29 | assert FibMemoOther.fibs(20) == "THE NUMBER 20 IS BORING" 30 | end 31 | 32 | test "identical function names with different arities return correct results" do 33 | FibMemo.fibs(20) 34 | FibMemoOther.fibs(20) 35 | FibMemoOther.fibs(20, 21) 36 | 37 | assert FibMemo.fibs(20) == 6765 38 | assert FibMemoOther.fibs(20) == "THE NUMBER 20 IS BORING" 39 | assert FibMemoOther.fibs(20, 21) == "21 AND 20 /2" 40 | end 41 | 42 | test "identical function names with guard conditions return correct results" do 43 | TestMemoWhen.fibs(20) 44 | TestMemoWhen.fibs("20") 45 | TestMemoWhen.fibs([1, 2, 3]) 46 | 47 | assert TestMemoWhen.fibs(20) == {:no_guard, 20} 48 | assert TestMemoWhen.fibs("20") == {:binary, "20"} 49 | assert TestMemoWhen.fibs([1, 2, 3]) == {:list, [1, 2, 3]} 50 | end 51 | 52 | test "normalized function arguments return correct results" do 53 | 54 | assert TestMemoNormalized.slow_upper("A") == TestMemoNormalized.slow_upper("a"), "single argument match" 55 | assert TestMemoNormalized.slow_upper("A") != TestMemoNormalized.slow_upper("B"), "single argument mis-match" 56 | 57 | assert TestMemoNormalized.slow_sum([1,2], false) == TestMemoNormalized.slow_sum([2,1], false), "multi-argument match" 58 | assert TestMemoNormalized.slow_sum([1,2], true) != TestMemoNormalized.slow_sum([2,1], false), "multi-argument mis-match" 59 | 60 | end 61 | 62 | test "normalized arguments performance improves" do 63 | 64 | {"AB", first_upper } = TimedFunction.time fn -> TestMemoNormalized.slow_upper("Ab") end 65 | {"AB", second_upper } = TimedFunction.time fn -> TestMemoNormalized.slow_upper("AB") end 66 | 67 | assert first_upper >= second_upper, "Second run on similar slow_upper arguments is faster" 68 | 69 | {9, first_sum } = TimedFunction.time fn -> TestMemoNormalized.slow_sum([2,3,4], false) end 70 | {9, second_sum } = TimedFunction.time fn -> TestMemoNormalized.slow_sum([4,2,3], false) end 71 | 72 | assert first_sum >= second_sum, "Second run on similar slow_sum arguments is faster" 73 | 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /test/defmemo_result_table_gs_test.exs: -------------------------------------------------------------------------------- 1 | 2 | defmodule DefMemo.ResultTable.GS.Test do 3 | use ExUnit.Case 4 | @doc """ 5 | Direct tests of the GenServer ResultTable. 6 | """ 7 | alias DefMemo.ResultTable.GS, as: RT 8 | 9 | @fstr {:"Elixir.TestMemoWhen", :fibs} 10 | 11 | @fib_memo {:"Elixir.FibMemo", :fibs} 12 | 13 | # === Basic Tests 14 | test "returns {:miss, nil} for unmemoed result" do 15 | assert RT.get(@fib_memo, [100]) == {:miss, nil} 16 | end 17 | 18 | test "returns {:hit, result} for a memo'd result" do 19 | FibMemo.fibs(20) 20 | assert RT.get(@fib_memo, [20]) == {:hit, 6765} 21 | end 22 | 23 | test "delete removes a memo'd result" do 24 | RT.delete(@fib_memo, [10]) 25 | assert RT.get(@fib_memo, [10]) == {:miss, nil} 26 | FibMemo.fibs(10) 27 | assert RT.get(@fib_memo, [10]) == {:hit, 55} 28 | RT.delete(@fib_memo, [10]) 29 | assert RT.get(@fib_memo, [10]) == {:miss, nil} 30 | end 31 | 32 | test "delete_all removed all memo'd results" do 33 | RT.delete_all 34 | assert RT.get(@fib_memo, [10]) == {:miss, nil} 35 | assert RT.get(@fib_memo, [20]) == {:miss, nil} 36 | FibMemo.fibs(10) 37 | FibMemo.fibs(20) 38 | assert RT.get(@fib_memo, [10]) == {:hit, 55} 39 | assert RT.get(@fib_memo, [20]) == {:hit, 6765} 40 | RT.delete_all 41 | assert RT.get(@fib_memo, [10]) == {:miss, nil} 42 | assert RT.get(@fib_memo, [20]) == {:miss, nil} 43 | end 44 | 45 | # Drying up the tests 46 | defp do_is_test(is_name, atom, test_value) do 47 | TestMemoWhen.fibs(test_value) 48 | assert RT.get(@fstr, [test_value]) == {:hit, {atom, test_value} }, is_name 49 | end 50 | 51 | test "returns correct result when is_binary" do 52 | do_is_test("is_binary", :binary, "20") 53 | end 54 | 55 | test "returns correctly when is_list" do 56 | do_is_test("is_list", :list, [1, 2, 3]) 57 | end 58 | 59 | test "returns correctly when is_atom" do 60 | do_is_test("is_atom", :atom, :test) 61 | end 62 | 63 | test "returns correctly when is_bitstring (caught by is_binary)" do 64 | do_is_test("is_binary", :binary, <<1, 0, 0, 0, 0, 0, 0, 0, 0>>) 65 | end 66 | 67 | test "returns correctly when is_boolean" do 68 | do_is_test("is_boolean", :boolean, true) 69 | end 70 | 71 | test "returns correctly when is_float" do 72 | do_is_test("is_float", :float, 3.14159265359) 73 | end 74 | 75 | test "returns correctly when is_function" do 76 | do_is_test("is_function", :function, fn(a) -> a * 2 end) 77 | end 78 | 79 | test "functions can be memoized!" do 80 | test_value = fn(a) -> a * 2 end 81 | do_is_test("is_function", :function, test_value) 82 | {:hit, {:function, fnc}} = RT.get(@fstr, [test_value] ) 83 | # functions can be memoized!? Useful if the key isnt the function itself... 84 | assert fnc.(2) == 4 85 | end 86 | 87 | test "returns correctly when is_map" do 88 | do_is_test("is_map", :map, %{:a => 1, :b => 2}) 89 | end 90 | 91 | test "returns correctly when is_pid" do 92 | do_is_test("is_pid", :pid, self) 93 | end 94 | 95 | test "returns correctly when is_port" do 96 | end 97 | 98 | test "returns correctly when is_reference" do 99 | end 100 | 101 | test "returns correctly when is_tuple" do 102 | do_is_test("is_tuple", :tuple, {1,2,3}) 103 | end 104 | 105 | test "#DefMemo.ResultTable.get returns correctly when is_list and is_binary" do 106 | TestMemoWhen.fibs([1, 2, 3], "TEST") 107 | assert RT.get(@fstr, [[1, 2, 3], "TEST"]) == {:hit, {[1, 2, 3], "TEST"} } 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/defmemo.ex: -------------------------------------------------------------------------------- 1 | defmodule DefMemo do 2 | @moduledoc """ 3 | Adapted from : (Gustavo Brunoro) https://gist.github.com/brunoro/6159378 4 | 5 | A simple DefMemo macro, the main point of note being that it can 6 | handle identical function signatures in differing modules. 7 | 8 | # See tests and test_helper for examples. 9 | """ 10 | use Application 11 | 12 | def start(_type, _args) do 13 | import Supervisor.Spec, warn: false 14 | 15 | children = [ worker(DefMemo.ResultTable.GS, []) ] 16 | 17 | Supervisor.start_link(children, 18 | [strategy: :one_for_one, 19 | name: DefMemo.ResultTable.Supervisor]) 20 | end 21 | 22 | alias DefMemo.ResultTable.GS, as: ResultTable 23 | 24 | defdelegate start_link, to: ResultTable 25 | 26 | @doc """ 27 | Defines a function as being memoized. Note that DefMemo.start_link 28 | must be called before calling a method defined with defmacro. 29 | 30 | # Example: 31 | defmodule FibMemo do 32 | import DefMemo 33 | 34 | defmemo fibs(0), do: 0 35 | defmemo fibs(1), do: 1 36 | defmemo fibs(n), do: fibs(n - 1) + fibs(n - 2) 37 | end 38 | 39 | A second argument can be provided to normalize the arguments for 40 | the memoization result lookup. The original function arguments are 41 | provided as a List to the normalization function. 42 | 43 | # Example: 44 | defmodule BadCaser do 45 | defp normalize_case([x]), do: String.downcase(x) 46 | defmemo slow_upper(s), normalize_case do: String.upcase(s) 47 | end 48 | 49 | This might realize time savings if `downcase` were significantly cheaper to 50 | execute than `upcase` or space savings if a wide variety of mixed-case, yet 51 | otherwise the same, strings were run through this code path. 52 | """ 53 | defmacro defmemo(head = {:when, _, [ {f_name, _, f_vars} | _ ] }, do: body) do 54 | quote do 55 | def unquote(head) do 56 | sig = {__MODULE__, unquote(f_name)} 57 | args = unquote(f_vars) 58 | 59 | case ResultTable.get(sig, args) do 60 | { :hit, value } -> value 61 | { :miss, nil } -> ResultTable.put(sig, args, unquote(body)) 62 | end 63 | end 64 | end 65 | end 66 | 67 | defmacro defmemo(head = {name, _, vars}, do: body) do 68 | quote do 69 | def unquote(head) do 70 | sig = {__MODULE__, unquote(name)} 71 | 72 | case ResultTable.get(sig, unquote(vars)) do 73 | { :hit, value } -> value 74 | { :miss, nil } -> ResultTable.put(sig, unquote(vars), unquote(body)) 75 | end 76 | end 77 | end 78 | end 79 | 80 | defmacro defmemo(head = {:when, _, [ {f_name, _, f_vars} | _ ] }, normalizer, do: body) do 81 | quote do 82 | def unquote(head) do 83 | sig = {__MODULE__, unquote(f_name)} 84 | args = unquote(f_vars) |> unquote(normalizer) 85 | 86 | case ResultTable.get(sig, args) do 87 | { :hit, value } -> value 88 | { :miss, nil } -> ResultTable.put(sig, args, unquote(body)) 89 | end 90 | end 91 | end 92 | end 93 | 94 | defmacro defmemo(head = {name, _, vars}, normalizer, do: body) do 95 | quote do 96 | def unquote(head) do 97 | sig = {__MODULE__, unquote(name)} 98 | 99 | args = unquote(vars) |> unquote(normalizer) 100 | 101 | case ResultTable.get(sig, args) do 102 | { :hit, value } -> value 103 | { :miss, nil } -> ResultTable.put(sig, args, unquote(body)) 104 | end 105 | end 106 | end 107 | end 108 | 109 | defmacro deathmemo(_) do 110 | quote do 111 | raise "Ryuk wants an apple!" 112 | end 113 | end 114 | end 115 | 116 | --------------------------------------------------------------------------------