LoginSignup
8
1

More than 1 year has passed since last update.

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

はじめに

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

Elixir標準のMap.merge/2

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

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

しかしながら、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"}}}

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

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

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

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

deep_merge (A)

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

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)

deep_merge (B)

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

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)

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)

deep_mergeKeyword型)

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

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)

deep_merge(Hexパッケージ)

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

iex

Mix.install([:deep_merge])

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

さいごに

Elixirを楽しみましょう!

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

image.png

image.png

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

image.png

8
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1