DEV Community 👩‍💻👨‍💻

DEV Community 👩‍💻👨‍💻 is a community of 963,274 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Masatoshi Nishiguchi
Masatoshi Nishiguchi

Posted on

ElixirのMapをdeep merge

ElixirのMapをdeep mergeする方法を調べたのでメモ。

はじめに

たまたまあるElixirのソースコードを読んでいたらdeep mergeしてたので面白いな〜と思い調べるに至りました。

Elixir標準のMap.merge/2

https://hexdocs.pm/elixir/Map.html#merge/3

Elixir言語には標準のMap.merge/2があります。便利です。

%{a: 1} |> Map.merge(%{b: 2}) |> Map.merge(%{c: 3})
# %{a: 1, b: 2, c: 3}
Enter fullscreen mode Exit fullscreen mode

しかしながら、Mapの構造が入れ子になっている場合、再帰的に統合することはしてくれません。

entry1 = %{"a" => %{"b" => %{"c1" => "hello", "c2" => "world" }}}
entry2 = %{"a" => %{"b" => %{"c1" => "コンニチハ", "c2" => "セカイ" }}}
entry3 = %{"a" => %{"b" => %{"c3" => "elixir" }}}

entry1 |> Map.merge(entry2) |> Map.merge(entry3)
# %{"a" => %{"b" => %{"c3" => "elixir"}}}
Enter fullscreen mode Exit fullscreen mode

欲しい結果が以下のようなものである場合、悩まされるかもしれません。

%{
  "a" => %{
    "b" => %{"c1" => "コンニチハ", "c2" => "セカイ", "c3" => "elixir"}
  }
}
Enter fullscreen mode Exit fullscreen mode

そこで登場するのがdeep_mergeです。

やり方はいくつか考えられます。

deep_merge (A)

今回の調査のきっかけとなった実装です。

https://github.com/fhunleth/beam_benchmarks/blob/6f970bff5b9291ffe2ddc849afbeabeea6595baf/lib/beam_benchmarks/info.ex#L89

defmodule MapUtils1 do
  def deep_merge(map1, map2) when is_map(map1) and is_map(map2) do
    Map.merge(map1, map2, fn _key, value1, value2 -> deep_merge(value1, value2) end)
  end

  def deep_merge(not_map1, not_map2), do: not_map2
end

entry1 |> MapUtils1.deep_merge(entry2) |> MapUtils1.deep_merge(entry3)
Enter fullscreen mode Exit fullscreen mode

deep_merge (B)

ネットで調べて出てきたのがこれです。

https://stackoverflow.com/questions/38864001/elixir-how-to-deep-merge-maps/38865647#38865647

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

  defp deep_resolve(_key, left = %{}, right = %{}) do
    deep_merge(left, right)
  end

  defp deep_resolve(_key, _left, right) do
    right
  end
end

entry1 |> MapUtils2.deep_merge(entry2) |> MapUtils2.deep_merge(entry3)
Enter fullscreen mode Exit fullscreen mode

deep_merge (C)

  • もっと簡潔に書けないかと思い、ボクノカンガエタサイキョウのdeep_mergeを書きました。
  • Frankさんの実装が一番簡潔といえばそうなのですが、関数を一個にまとめてみたかったですし、あと個人的に無名関数でのパターンマッチングの見た目がかっこいいと思っているんです。
defmodule MapUtils3 do
  def deep_merge(left, right) do
    Map.merge(left, right, fn
      _key, l = %{}, r = %{} -> deep_merge(l, r)
      _key, _l, r ->  r
    end)
  end
end

entry1 |> MapUtils3.deep_merge(entry2) |> MapUtils3.deep_merge(entry3)
Enter fullscreen mode Exit fullscreen mode

deep_merge(Keyword型)

  • Configモジュールの内部にKeyword型の設定データをdeep_mergeする__merge__/2という関数がありました。
  • deep_mergeをKeyword型に対して実施したい場合に参考になるかもしれません。

https://github.com/elixir-lang/elixir/blob/6764de46cbd21003c54a0072cc3b9a6d9dffea16/lib/elixir/lib/config.ex#L328-L340

deep_merge(Keyword型とMap型両方)

  • 必要であればMap型とKeyword型の両方に対応することもできそうです。
defmodule MapUtils5 do
  def deep_merge(map1, map2) when is_map(map1) and is_map(map2) do
    Map.merge(map1, map2, fn _key, value1, value2 -> deep_merge(value1, value2) end)
  end

  def deep_merge(kw1, kw2) when is_list(kw1) and is_list(kw2) do
    Keyword.merge(kw1, kw2, fn _key, value1, value2 -> deep_merge(value1, value2) end)
  end

  def deep_merge(_not_map1, not_map2), do: not_map2
end

# Map
entry1 = %{"a" => %{"b" => %{"c1" => "hello", "c2" => "world" }}}
entry2 = %{"a" => %{"b" => %{"c1" => "コンニチハ", "c2" => "セカイ" }}}
entry3 = %{"a" => %{"b" => %{"c3" => "elixir" }}}

entry1 |> MapUtils5.deep_merge(entry2) |> MapUtils5.deep_merge(entry3)

# Keyword
entry1 = [a: [b: [c1: "hello", c2: "world" ]]]
entry2 = [a: [b: [c1: "コンニチハ", c2: "セカイ" ]]]
entry3 = [a: [b: [c3: "elixir" ]]]

entry1 |> MapUtils5.deep_merge(entry2) |> MapUtils5.deep_merge(entry3)
Enter fullscreen mode Exit fullscreen mode

deep_merge(Hexパッケージ)

  • deep_merge関連の色んな機能がHexパッケージにされています。
  • ほとんどの場合、上述の知識を持って自分で関数を書いた方が早いと思いますが、シンプルな実装では対処できない特殊な場合に活躍するようです。

https://github.com/PragTob/deep_merge

iex

Mix.install([:deep_merge])

entry1 |> DeepMerge.deep_merge(entry2) |> DeepMerge.deep_merge(entry3)
Enter fullscreen mode Exit fullscreen mode

さいごに

Elixirを楽しみましょう!

https://qiita.com/piacerex/items/e5590fa287d3c89eeebf

https://qiita.com/torifukukaiou/items/1edb3e961acf002478fd

各コミュニティの詳細は、「Elixirコミュニティの歩き方 -国内オンライン編-」をご覧ください

image.png

image.png

各種Elixirイベントの開催予定は、「Elixirイベントカレンダー」から確認/参加できます 📆

image.png

Top comments (0)

🌚 Life is too short to browse without dark mode