From 07f9160c4bd962bb0f1ec9d266f4dfd4ca5e9c3e Mon Sep 17 00:00:00 2001 From: Tobias Pfeiffer Date: Wed, 19 Oct 2016 12:42:54 +0200 Subject: [PATCH] Implement Map.deep_merge/2 and Map.deep_merge/3 * as proposed in https://groups.google.com/forum/#!topic/elixir-lang-core/ak3kVqJ4-8g but with a concrete implementation to reason about :) --- lib/elixir/lib/map.ex | 71 +++++++++++++++++++++++++ lib/elixir/test/elixir/keyword_test.exs | 2 +- lib/elixir/test/elixir/map_test.exs | 27 ++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/map.ex b/lib/elixir/lib/map.ex index 91641b29df0..25feaffc648 100644 --- a/lib/elixir/lib/map.ex +++ b/lib/elixir/lib/map.ex @@ -473,6 +473,77 @@ defmodule Map do end, map1, map2 end + @doc """ + Merges two maps similar to `Map.merge/2`, but with the important difference + that if keys exist in both maps and their value is also a map it will + recursively also merge these maps. If a value for a duplicated key is not a + map it will prefer the value present in `override`. + + For maps without maps as possible values it behaves exactly like `Map.merge/2` + + ## Example + + iex> Map.deep_merge(%{a: 1, b: %{x: 10, y: 9}}, %{b: %{y: 20, z: 30}, c: 4}) + %{a: 1, b: %{x: 10, y: 20, z: 30}, c: 4} + + iex> Map.deep_merge(%{a: 1, b: 2}, %{b: 3, c: 4}) + %{a: 1, b: 3, c: 4} + + iex> Map.deep_merge(%{a: 1, b: %{x: 10, y: 9}}, %{b: 5, c: 4}) + %{a: 1, b: 5, c: 4} + + iex> Map.deep_merge(%{a: 1, b: 5}, %{b: %{x: 10, y: 9}, c: 4}) + %{a: 1, b: %{x: 10, y: 9}, c: 4} + + iex> Map.deep_merge(%{a: %{b: %{c: %{d: "foo", e: 2}}}}, %{a: %{b: %{c: %{d: "bar"}}}}) + %{a: %{b: %{c: %{d: "bar", e: 2}}}} + + """ + @spec deep_merge(map, map) :: map + def deep_merge(base_map, override) do + deep_merge(base_map, override, fn(_key, _base, override) -> override end) + end + + @doc """ + And adjustable version of `Map.deep_merge/2` where one can specify the + resolver function akin to `Map.merge/3` which gets called if a key exists in + both maps and is not a map. + + This can also be practical if you want to merge further values like lists. + + ## Examples + + iex> Map.deep_merge(%{a: %{y: "bar", z: "bar"}, b: 2}, %{a: %{y: "foo"}, b: 3, c: 4}, fn(_, _, _) -> :conflict end) + %{a: %{y: :conflict, z: "bar"}, b: :conflict, c: 4} + + iex> simple_resolver = fn(_key, base, _override) -> base end + iex> Map.deep_merge(%{a: 1, b: %{x: 10, y: 9}}, %{b: 5, c: 4}, simple_resolver) + %{a: 1, b: %{x: 10, y: 9}, c: 4} + + iex> list_merger = fn + ...> _, list1, list2 when is_list(list1) and is_list(list2) -> + ...> list1 ++ list2 + ...> _, _, override -> + ...> override + ...> end + iex> Map.deep_merge(%{a: %{b: [1]}, c: 2}, %{a: %{b: [2]}, c: 100}, list_merger) + %{a: %{b: [1, 2]}, c: 100} + + """ + @spec deep_merge(map, map, (key, value, value -> value)) :: map + def deep_merge(base_map, override_map, fun) do + Map.merge base_map, override_map, build_deep_merge_resolver(fun) + end + + defp build_deep_merge_resolver(fun) do + fn + _key, base_map, override_map when is_map(base_map) and is_map(override_map) -> + deep_merge(base_map, override_map, fun) + key, base, override -> + fun.(key, base, override) + end + end + @doc """ Updates the `key` in `map` with the given function. diff --git a/lib/elixir/test/elixir/keyword_test.exs b/lib/elixir/test/elixir/keyword_test.exs index 412977d82cb..1b0be0f1138 100644 --- a/lib/elixir/test/elixir/keyword_test.exs +++ b/lib/elixir/test/elixir/keyword_test.exs @@ -24,7 +24,7 @@ defmodule KeywordTest do test "implements (almost) all functions in Map" do assert Map.__info__(:functions) -- Keyword.__info__(:functions) == - [from_struct: 1] + [deep_merge: 2, deep_merge: 3, from_struct: 1] end test "get_and_update/3 raises on bad return value from the argument function" do diff --git a/lib/elixir/test/elixir/map_test.exs b/lib/elixir/test/elixir/map_test.exs index 1395b4bdead..1b9eec4f26b 100644 --- a/lib/elixir/test/elixir/map_test.exs +++ b/lib/elixir/test/elixir/map_test.exs @@ -236,4 +236,31 @@ defmodule MapTest do assert LocalUser.new == %LocalUser{name: "john", nested: %LocalUser.NestedUser{}} assert LocalUser.Context.new == %LocalUser{name: "john", nested: %LocalUser.NestedUser{}} end + + test "deep_merge/3 with a simple custom resolver" do + res = + Map.deep_merge %{a: %{b: [1]}}, %{a: %{b: [2]}}, + fn(_key, val1, val2) -> + val1 ++ val2 + end + + assert res == %{a: %{b: [1, 2]}} + end + + test "deep_merge/3 with keyword lists arrays and numbers" do + resolver = fn + _, list1, list2 when is_list(list1) and is_list(list2) -> + Keyword.merge(list1, list2) + _, num1, num2 when is_number(num1) and is_number(num2) -> + num1 + num2 + _, val1, _val2 -> + val1 + end + + res = Map.deep_merge %{a: 1, b: [a: 1, c: 3], d: "foo"}, + %{a: 2, b: [c: 10, d: 4], d: "bar"}, + resolver + + assert res == %{a: 3, b: [a: 1, c: 10, d: 4], d: "foo"} + end end