2016-08-10 15 views
5

Z Map.merge mam:Eliksir - jak pogrupować mapy?

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

ale faktycznie chcę:

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

Czy istnieje metoda naturalnej bez pisanie funkcji rekurencyjnej szablonowe w tym przypadku?

Odpowiedz

10

Jak zasugerował @Dogbert, możesz napisać funkcję rekurencyjnego scalania map.

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 

Oto niektóre przypadki testowe, aby dać wyobrażenie, jak konflikty są rozwiązywane:

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 
+0

Wielkie dzięki! – asiniy

+0

Drobiazg, o którym należy pamiętać, jest to, że po cichu scalą one typy zdefiniowane przez strukturę/niestandardowe, a także wewnątrz są po prostu mapami. Aby tego uniknąć, możesz dopasować wzór do klucza '__struct__', który jest obecny, gdy jest to struktura. Aby uniknąć wpadnięcia w te pułapki, napisałem też bibliotekę [deep_merge] (https://github.com/PragTob/deep_merge). – PragTob

5

Jeśli masz tylko 1 poziom zagnieżdżenia map środku mapy, a wszystkie wartości mapie najwyższego poziomu są mapy, można użyć 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}} 

nieskończoną zagnieżdżenia, wierzę pisanie funkcji jest jedyny sposób, ale w tej funkcji możesz użyć Map.merge/3, aby zmniejszyć jakiś kod.

1

Jak już wspomniano w komentarzu naiwne podejście do deep_merge również przypadkowo scala wszystkie kodowanym/typów niestandardowych jak oni są mapami wewnętrznie. Zrobiłem ten sam błąd i zaimplementowałem bibliotekę deep_merge, aby zapobiec tym błędom i zapewnić dodatkowe funkcje.

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] 

To ma kilka dodatkowych funkcji, które może (lub nie może) potrzeba:

  • Obsługuje zarówno mapy i listy słów kluczowych
  • To nie scala konstrukcjom lub mapy z kodowanym ...
  • ... ale możesz zaimplementować prosty protokół DeepMerge.Resolver dla wybranych typów/struktur, aby je również scalić głęboko.
  • deep_merge/3 wariant, który otrzymuje funkcję podobną do Map.merge/3 zmodyfikować zachowanie łączących, na przykład w przypadku, gdy nie chcesz listy słów kluczowych, aby zostać połączone lub chcesz wszystkie listy, które należy dołączyć
0

Jest podobny kod w źródle Mix.Config: https://github.com/elixir-lang/elixir/blob/master/lib/mix/lib/mix/config.ex#L302

@doc """ 
Merges two configurations. 

The configuration of each application is merged together 
with the values in the second one having higher preference 
than the first in case of conflicts. 

## Examples 

    iex> Mix.Config.merge([app: [k: :v1]], [app: [k: :v2]]) 
    [app: [k: :v2]] 

    iex> Mix.Config.merge([app1: []], [app2: []]) 
    [app1: [], app2: []] 

""" 
def merge(config1, config2) do 
    Keyword.merge(config1, config2, fn _, app1, app2 -> 
    Keyword.merge(app1, app2, &deep_merge/3) 
    end) 
end 

defp deep_merge(_key, value1, value2) do 
    if Keyword.keyword?(value1) and Keyword.keyword?(value2) do 
    Keyword.merge(value1, value2, &deep_merge/3) 
    else 
    value2 
    end 
end 
Powiązane problemy