ElixirのMapをdeep merge
Tags: elixir, 40代駆け出しエンジニア, adventcalendar2022
Published: 2022-09-18 01:05:02

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

## はじめに

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

## Elixir標準の`Map.merge/2`

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

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

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

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

```elixir
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"}}}
```

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

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

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

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

## `deep_merge` （A）

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

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

```elixir
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）

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

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

```elixir
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さんの実装が一番簡潔といえばそうなのですが、関数を一個にまとめてみたかったですし、あと個人的に無名関数でのパターンマッチングの見た目がかっこいいと思っているんです。

```elixir
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_merge`（`Keyword`型）

- [`Config`](https://hexdocs.pm/elixir/main/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`型の両方に対応することもできそうです。

```elixir
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パッケージにされています。
- ほとんどの場合、上述の知識を持って自分で関数を書いた方が早いと思いますが、シンプルな実装では対処できない特殊な場合に活躍するようです。

https://github.com/PragTob/deep_merge

```elixir
iex

Mix.install([:deep_merge])

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

## さいごに

Elixirを楽しみましょう！

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

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

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

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/f891b7ad-d2c4-3303-915b-f831069e28a4.png)](https://speakerdeck.com/elijo/elixirkomiyunitei-falsebu-kifang-guo-nei-onrainbian)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/7fdc5db7-dfad-9d10-28f8-1e0b8830a587.png)

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

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109744/985acaa4-50c9-da42-ae32-50fbf9119e61.png)](https://elixir-jp-calendar.fly.dev/)