DEV Community

Masatoshi Nishiguchi
Masatoshi Nishiguchi

Posted on

3

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_mergeKeyword型)

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

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

deep_mergeKeyword型と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

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs