Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elixir - how to deep merge maps?

Tags:

elixir

With Map.merge I have:

Map.merge(%{ a: %{ b: 1 }}, %{ a: %{ c: 3 }}) # => %{ a: %{ c: 3 }}

but actually I want to:

Map.merge(%{ a: %{ b: 1 }}, %{ a: %{ c: 3 }}) # => %{ a: %{ b: 1, c: 3 }}

Is there any native method without writing a recursive boilerplate function for this case?

like image 259
asiniy Avatar asked Aug 10 '16 04:08

asiniy


3 Answers

As @Dogbert suggested, you can write a function to recursively merge maps.

defmodule MapUtils do
  def deep_merge(left, right) do
    Map.merge(left, right, &deep_resolve/3)
  end

  # Key exists in both maps, and both values are maps as well.
  # These can be merged recursively.
  defp deep_resolve(_key, left = %{}, right = %{}) do
    deep_merge(left, right)
  end

  # Key exists in both maps, but at least one of the values is
  # NOT a map. We fall back to standard merge behavior, preferring
  # the value on the right.
  defp deep_resolve(_key, _left, right) do
    right
  end
end

Here are some test cases to give you an idea how conflicts are resolved:

ExUnit.start

defmodule MapUtils.Test do
  use ExUnit.Case

  test 'one level of maps without conflict' do
    result = MapUtils.deep_merge(%{a: 1}, %{b: 2})
    assert result == %{a: 1, b: 2}
  end

  test 'two levels of maps without conflict' do
    result = MapUtils.deep_merge(%{a: %{b: 1}}, %{a: %{c: 3}})
    assert result == %{a: %{b: 1, c: 3}}
  end

  test 'three levels of maps without conflict' do
    result = MapUtils.deep_merge(%{a: %{b: %{c: 1}}}, %{a: %{b: %{d: 2}}})
    assert result == %{a: %{b: %{c: 1, d: 2}}}
  end

  test 'non-map value in left' do
    result = MapUtils.deep_merge(%{a: 1}, %{a: %{b: 2}})
    assert result == %{a: %{b:  2}}
  end

  test 'non-map value in right' do
    result = MapUtils.deep_merge(%{a: %{b: 1}}, %{a: 2})
    assert result == %{a: 2}
  end

  test 'non-map value in both' do
    result = MapUtils.deep_merge(%{a: 1}, %{a: 2})
    assert result == %{a: 2}
  end
end
like image 163
Patrick Oscity Avatar answered Oct 19 '22 03:10

Patrick Oscity


As just mentioned in a comment the naive approach to deep_merge also accidentally merges all structs/custom types as they are maps internally. I made the same mistake and so implemented a deep_merge library to prevent these mistakes and provide further features.

DeepMerge.deep_merge original_map, other_map

iex> DeepMerge.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> DeepMerge.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]

It has a couple of extra features you might (or might not) need:

  • It handles both maps and keyword lists
  • It does not merge structs or maps with structs…
  • …but you can implement the simple DeepMerge.Resolver protocol for types/structs of your choice to also make them deep mergable
  • a deep_merge/3 variant that gets a function similar to Map.merge/3 to modify the merging behavior, for instance in case you don't want keyword lists to be merged or you want all lists to be appended
like image 3
PragTob Avatar answered Oct 19 '22 04:10

PragTob


If you only have 1 level nesting of maps inside maps, and all the values of the top level map are maps, you can use Map.merge/3:

iex(1)> a = %{ a: %{ b: 1 }}
%{a: %{b: 1}}
iex(2)> b = %{ a: %{ c: 3 }}
%{a: %{c: 3}}
iex(3)> Map.merge(a, b, fn _, a, b -> Map.merge(a, b) end)
%{a: %{b: 1, c: 3}}

For infinite nesting, I believe writing a function is the only way but in that function you can use Map.merge/3 to reduce some code.

like image 12
Dogbert Avatar answered Oct 19 '22 03:10

Dogbert