From 644b765a3d4577aa0863819c03cdc8b0f307b76e Mon Sep 17 00:00:00 2001 From: Devin Torres Date: Tue, 15 Jan 2013 17:17:52 -0600 Subject: [PATCH] Introduce List.Dict, DRYing the tuple list as Dict implementation --- lib/elixir/lib/dict.ex | 168 +++++++-------------------- lib/elixir/lib/list/dict.ex | 112 ++++++++++++++++++ lib/elixir/test/elixir/dict_test.exs | 85 ++++++-------- 3 files changed, 186 insertions(+), 179 deletions(-) create mode 100644 lib/elixir/lib/list/dict.ex diff --git a/lib/elixir/lib/dict.ex b/lib/elixir/lib/dict.ex index b29b8ec5f8c..a1161630584 100644 --- a/lib/elixir/lib/dict.ex +++ b/lib/elixir/lib/dict.ex @@ -5,12 +5,6 @@ defmodule Dict do functions that redirect to the underlying Dict based on the tuple signature. - The keyword list used throughout Elixir cannot be - manipulated via the Dict module, you must use the - Keyword module instead. This distinction is intentional: - the Dict module is meant to work on structures that work - as storage. - To create a new dict, use the `new` functions defined by each dict type: @@ -45,6 +39,17 @@ defmodule Dict do defcallback update(t, key, (value -> value)) :: t defcallback values(t) :: list(value) + defmacrop target(dict) do + quote do + cond do + is_tuple(unquote(dict)) -> + elem(unquote(dict), 0) + is_list(unquote(dict)) -> + List.Dict + end + end + end + @doc """ Returns a list containing all dict's keys. The keys are not guaranteed to be sorted, unless @@ -57,12 +62,8 @@ defmodule Dict do """ @spec keys(t) :: [key] - def keys(dict) when is_tuple(dict) do - elem(dict, 0).keys(dict) - end - - def keys(dict) when is_list(dict) do - lc { key, _ } inlist dict, do: key + def keys(dict) do + target(dict).keys(dict) end @doc """ @@ -75,12 +76,8 @@ defmodule Dict do """ @spec values(t) :: [value] - def values(dict) when is_tuple(dict) do - elem(dict, 0).values(dict) - end - - def values(dict) when is_list(dict) do - lc { _, value } inlist dict, do: value + def values(dict) do + target(dict).values(dict) end @doc """ @@ -93,12 +90,8 @@ defmodule Dict do """ @spec size(t) :: non_neg_integer - def size(dict) when is_tuple(dict) do - elem(dict, 0).size(dict) - end - - def size(dict) when is_list(dict) do - length(dict) + def size(dict) do + target(dict).size(dict) end @doc """ @@ -112,12 +105,8 @@ defmodule Dict do """ @spec has_key?(t, key) :: boolean - def has_key?(dict, key) when is_tuple(dict) do - elem(dict, 0).has_key?(dict, key) - end - - def has_key?(dict, key) when is_list(dict) do - :lists.keymember(key, 1, dict) + def has_key?(dict, key) do + target(dict).has_key?(dict, key) end @doc """ @@ -133,17 +122,8 @@ defmodule Dict do """ @spec get(t, key, value) :: value - def get(dict, key, default // nil) - - def get(dict, key, default) when is_tuple(dict) do - elem(dict, 0).get(dict, key, default) - end - - def get(dict, key, default) when is_list(dict) do - case :lists.keyfind(key, 1, dict) do - { ^key, value } -> value - false -> default - end + def get(dict, key, default // nil) do + target(dict).get(dict, key, default) end @doc """ @@ -158,15 +138,8 @@ defmodule Dict do """ @spec get!(t, key) :: value | no_return - def get!(dict, key) when is_tuple(dict) do - elem(dict, 0).get!(dict, key) - end - - def get!(dict, key) when is_list(dict) do - case :lists.keyfind(key, 1, dict) do - { ^key, value } -> value - false -> raise(KeyError, key: key) - end + def get!(dict, key) do + target(dict).get!(dict, key) end @doc """ @@ -181,12 +154,8 @@ defmodule Dict do """ @spec put(t, key, value) :: t - def put(dict, key, val) when is_tuple(dict) do - elem(dict, 0).put(dict, key, val) - end - - def put(dict, key, val) when is_list(dict) do - [{key, val}|delete(dict, key)] + def put(dict, key, val) do + target(dict).put(dict, key, val) end @doc """ @@ -200,15 +169,8 @@ defmodule Dict do """ @spec put_new(t, key, value) :: t - def put_new(dict, key, val) when is_tuple(dict) do - elem(dict, 0).put_new(dict, key, val) - end - - def put_new(dict, key, val) when is_list(dict) do - case :lists.keyfind(key, 1, dict) do - { ^key, _ } -> dict - false -> [{key,val}|dict] - end + def put_new(dict, key, val) do + target(dict).put_new(dict, key, val) end @doc """ @@ -225,12 +187,8 @@ defmodule Dict do """ @spec delete(t, key) :: t - def delete(dict, key) when is_tuple(dict) do - elem(dict, 0).delete(dict, key) - end - - def delete(dict, key) when is_list(dict) do - lc { k, _ } = tuple inlist dict, key != k, do: tuple + def delete(dict, key) do + target(dict).delete(dict, key) end @doc """ @@ -248,10 +206,6 @@ defmodule Dict do """ @spec merge(t, t) :: t - def merge(dict1, dict2) when is_list(dict1) and is_list(dict2) do - dict2 ++ lc({ k, _ } = tuple inlist dict1, not has_key?(dict2, k), do: tuple) - end - def merge(dict1, dict2) do merge(dict1, dict2, fn(_k, _v1, v2) -> v2 end) end @@ -271,20 +225,8 @@ defmodule Dict do """ @spec merge(t, t, (key, value, value -> value)) :: t - def merge(dict1, dict2, fun) when is_tuple(dict1) do - elem(dict1, 0).merge(dict1, dict2, fun) - end - - def merge(dict1, dict2, fun) when is_list(dict1) and is_list(dict2) do - do_merge(dict2, dict1, fun) - end - - defp do_merge([{ k, v2 }|t], acc, fun) do - do_merge t, update(acc, k, v2, fn(v1) -> fun.(k, v1, v2) end), fun - end - - defp do_merge([], acc, _fun) do - acc + def merge(dict1, dict2, fun) do + target(dict1).merge(dict1, dict2, fun) end @doc """ @@ -299,20 +241,8 @@ defmodule Dict do """ @spec update(t, key, (value -> value)) :: t - def update(dict, key, fun) when is_tuple(dict) do - elem(dict, 0).update(dict, key, fun) - end - - def update([{key, value}|dict], key, fun) do - [{key, fun.(value)}|delete(dict, key)] - end - - def update([{_, _} = e|dict], key, fun) do - [e|update(dict, key, fun)] - end - - def update([], key, _fun) do - raise(KeyError, key: key) + def update(dict, key, fun) do + target(dict).update(dict, key, fun) end @doc """ @@ -328,32 +258,16 @@ defmodule Dict do """ @spec update(t, key, value, (value -> value)) :: t - def update(dict, key, initial, fun) when is_tuple(dict) do - elem(dict, 0).update(dict, key, initial, fun) - end - - def update([{key, value}|dict], key, _initial, fun) do - [{key, fun.(value)}|delete(dict, key)] - end - - def update([{_, _} = e|dict], key, initial, fun) do - [e|update(dict, key, initial, fun)] - end - - def update([], key, initial, _fun) do - [{key, initial}] + def update(dict, key, initial, fun) do + target(dict).update(dict, key, initial, fun) end @doc """ Returns an empty dict of the same type as `dict`. """ @spec empty(t) :: t - def empty(dict) when is_tuple(dict) do - elem(dict, 0).empty(dict) - end - - def empty(dict) when is_list(dict) do - [] + def empty(dict) do + target(dict).empty(dict) end @doc """ @@ -361,11 +275,7 @@ defmodule Dict do No particular order is enforced. """ @spec to_list(t) :: list - def to_list(dict) when is_tuple(dict) do - elem(dict, 0).to_list(dict) - end - - def to_list(dict) when is_list(dict) do - dict + def to_list(dict) do + target(dict).to_list(dict) end end diff --git a/lib/elixir/lib/list/dict.ex b/lib/elixir/lib/list/dict.ex new file mode 100644 index 00000000000..ff403369f7d --- /dev/null +++ b/lib/elixir/lib/list/dict.ex @@ -0,0 +1,112 @@ +defmodule List.Dict do + @doc false + def new, do: [] + + @doc false + def new(pairs) do + Enum.reduce pairs, new, fn { k, v }, acc -> + [ {k, v} | acc ] + end + end + + @doc false + def new(list, transform) when is_function(transform) do + Enum.reduce list, [], fn i, acc -> + { k, v } = transform.(i) + [ {k, v} | acc ] + end + end + + @doc false + def keys(dict) do + lc { key, _ } inlist dict, do: key + end + + @doc false + def values(dict) do + lc { _, value } inlist dict, do: value + end + + @doc false + def size(dict) do + length(dict) + end + + @doc false + def has_key?(dict, key) do + :lists.keymember(key, 1, dict) + end + + @doc false + def get(dict, key, default) do + case :lists.keyfind(key, 1, dict) do + { ^key, value } -> value + false -> default + end + end + + @doc false + def get!(dict, key) do + case :lists.keyfind(key, 1, dict) do + { ^key, value } -> value + false -> raise(KeyError, key: key) + end + end + + @doc false + def put(dict, key, val) do + [{key, val}|delete(dict, key)] + end + + @doc false + def put_new(dict, key, val) do + case :lists.keyfind(key, 1, dict) do + { ^key, _ } -> dict + false -> [{key,val}|dict] + end + end + + @doc false + def delete(dict, key) do + lc { k, _ } = tuple inlist dict, key != k, do: tuple + end + + @doc false + def merge(dict1, dict2, fun) do + Enum.reduce dict2, dict1, fn { k, v2 }, acc -> + update(acc, k, v2, fn(v1) -> fun.(k, v1, v2) end) + end + end + + @doc false + def update([{key, value}|dict], key, fun) do + [{key, fun.(value)}|delete(dict, key)] + end + + def update([{_, _} = e|dict], key, fun) do + [e|update(dict, key, fun)] + end + + def update([], key, _fun) do + raise(KeyError, key: key) + end + + @doc false + def update([{key, value}|dict], key, _initial, fun) do + [{key, fun.(value)}|delete(dict, key)] + end + + def update([{_, _} = e|dict], key, initial, fun) do + [e|update(dict, key, initial, fun)] + end + + def update([], key, initial, _fun) do + [{key, initial}] + end + + @doc false + def empty(_dict), do: [] + + @doc false + def to_list(dict), do: dict +end diff --git a/lib/elixir/test/elixir/dict_test.exs b/lib/elixir/test/elixir/dict_test.exs index b89b0b071ac..78f4e0c806b 100644 --- a/lib/elixir/test/elixir/dict_test.exs +++ b/lib/elixir/test/elixir/dict_test.exs @@ -1,10 +1,19 @@ Code.require_file "../test_helper.exs", __FILE__ defmodule DictTest.Common do - defmacro __using__(_) do + defmacro __using__(module) do quote location: :keep do use ExUnit.Case, async: true + # Most underlying Dict implementations have no key order guarantees, + # sort them before we compare: + defmacrop dicts_equal(actual, expected) do + quote do + cmp = fn { k1, _ }, { k2, _ } -> k1 < k2 end + Enum.sort(expected, cmp) == Enum.sort(actual, cmp) + end + end + test :access do dict = new_dict [{"first_key", 1}, {"second_key", 2}] assert dict["first_key"] == 1 @@ -83,34 +92,25 @@ defmodule DictTest.Common do dict1 = new_dict Enum.zip ["a", "b", "c"], [1, 2, 3] dict2 = new_dict Enum.zip ["a", "c", "d"], [3, :a, 0] - merged = Dict.merge(dict1, dict2) - final = new_dict Enum.zip ["a", "b", "c", "d"], [3, 2, :a, 0] - - cmp = fn {k1, _}, {k2, _} -> k1 < k2 end - actual = Enum.sort(Dict.to_list(merged), cmp) - expected = Enum.sort(final, cmp) - assert expected == actual + actual = Dict.merge(dict1, dict2) + expected = new_dict Enum.zip ["a", "b", "c", "d"], [3, 2, :a, 0] + assert dicts_equal actual, expected end test :merge_with_enum do dict1 = new_dict Enum.zip ["a", "b", "c"], [1, 2, 3] dict2 = Enum.zip ["a", "c", "d"], [3, :a, 0] - merged = Dict.merge(dict1, dict2) - final = new_dict(Enum.zip ["a", "b", "c", "d"], [3, 2, :a, 0]) - - cmp = fn {k1, _}, {k2, _} -> k1 < k2 end - actual = Enum.sort(Dict.to_list(merged), cmp) - expected = Enum.sort(final, cmp) - assert expected == actual + actual = Dict.merge(dict1, dict2) + expected = new_dict(Enum.zip ["a", "b", "c", "d"], [3, 2, :a, 0]) + assert dicts_equal actual, expected end test :merge_with_function do dict1 = new_dict Enum.zip ["a", "b"], [1, 2] dict2 = new_dict Enum.zip ["a", "d"], [3, 4] - result = Dict.merge dict1, dict2, fn _k, v1, v2 -> - v1 + v2 - end - assert new_dict(Enum.zip ["a", "b", "d"], [4, 2, 4]) == result + actual = Dict.merge dict1, dict2, fn _k, v1, v2 -> v1 + v2 end + expected = new_dict(Enum.zip ["a", "b", "d"], [4, 2, 4]) + assert dicts_equal actual, expected end test :has_key do @@ -135,40 +135,34 @@ defmodule DictTest.Common do test :empty do assert empty_dict == Dict.empty new_dict end + + defp empty_dict, do: unquote(module).new + + defp new_dict(list // [{"first_key", 1}, {"second_key", 2}]) + defp new_dict(list), do: unquote(module).new list + defp new_dict(list, transform), do: unquote(module).new list, transform end end end -defmodule DictTest do - use DictTest.Common +defmodule HashDictTest do + use DictTest.Common, HashDict test :new do assert :dict.new == elem(new_dict([]), 1) end - - defp empty_dict, do: HashDict.new - - defp new_dict(list // [{"first_key", 1}, {"second_key", 2}]) - defp new_dict(list), do: HashDict.new list - defp new_dict(list, transform), do: HashDict.new list, transform end defmodule OrdDictTest do - use DictTest.Common + use DictTest.Common, OrdDict test :new do assert [] == elem(new_dict([]), 1) end - - defp empty_dict, do: OrdDict.new - - defp new_dict(list // [{"first_key", 1}, {"second_key", 2}]) - defp new_dict(list), do: OrdDict.new list - defp new_dict(list, transform), do: OrdDict.new list, transform end defmodule Binary.DictTest do - use DictTest.Common + use DictTest.Common, Binary.Dict test :new do assert [] == elem(new_dict([]), 1) @@ -179,22 +173,13 @@ defmodule Binary.DictTest do assert merged[:first_key] == 13 assert merged["first_key"] == 13 end - - defp empty_dict, do: Binary.Dict.new - - defp new_dict(list // [{"first_key", 1}, {"second_key", 2}]) - defp new_dict(list), do: Binary.Dict.new list - defp new_dict(list, transform), do: Binary.Dict.new list, transform end -defmodule ListDictTest do - use DictTest.Common - - defp empty_dict, do: [] +defmodule List.DictTest do + use DictTest.Common, List.Dict - defp new_dict(list // [{"first_key", 1}, {"second_key", 2}]) - defp new_dict(list), do: list - defp new_dict(list, transform) do - Enum.map list, transform + test :merge_mixed do + merged = Dict.merge(new_dict, HashDict.new [first_key: 13]) + assert merged[:first_key] == 13 end -end \ No newline at end of file +end