【TIPS】マップリストの変形と更新をマスターする

この記事は、[Elixir Advent Calendar 2023 シリーズ10](https://qiita.com/advent-calendar/2023/elixir) の20日目です

---

【本コラムは、10分で読め、10分で試せます】

[piacere](https://twitter.com/piacere_ex) です、ご覧いただいてありがとございます :bow:

下記のようなマップリストは、同列の複数データを扱う際にとても便利で、RepoによるDBデータ取得も基本的にこの形です

```diff_elixir
users = [
  %{id: :id000, name: "hoge", weight: 49}, 
  %{id: :id001, name: "foo",  weight: nil}, 
  %{id: :id002, name: "fuga", weight: 33}
]
```

しかし、ネストした構造のため、中身の更新が面倒な印象があります

また、マップキーワードリストと異なり、[1つ前のコラムで示した `put_in`](https://qiita.com/piacerex/items/1fc9cbbb10faae059d65) も使えません

そこを様々な方法でどうにかしていきたいと思います

# マップリストの変形

まず更新を行う前に、基本的な変形から整理します

## 全項目を2重リスト化

```diff_elixir
users2 = 
users
|> Enum.map(& Enum.into(&1, []))
[
+ [id: :id000, name: "hoge", weight: 49],
+ [id: :id001, name: "foo", weight: nil],
+ [id: :id002, name: "fuga", weight: 33]
]
```

```diff_elixir:使い方
iex> users2 |> Enum.filter(& &1[:id] == :id001) |> Enum.flat_map(& &1)
[id: :id001, name: "foo", weight: nil]

iex> users2 |> Enum.map(& if String.contains?(&1[:name], "f"), do: String.replace(&1[:name], "o", ""), else: &1[:name])
["hoge", "f", "fuga"]
```

戻すのもカンタンです

```diff_elixir
users2 
|> Enum.map(& Enum.into(&1, %{}))
[
+ %{id: :id000, name: "hoge", weight: 49},
+ %{id: :id001, name: "foo", weight: nil},
+ %{id: :id002, name: "fuga", weight: 33}
]
```

## 項目をマップの外に出すマップキーワードリスト化

```diff_elixir
users3 = 
users|> Enum.map(& {&1.id, Map.drop(&1, [:id])})
[
+ id000: %{name: "hoge", weight: 49},
+ id001: %{name: "foo", weight: nil},
+ id002: %{name: "fuga", weight: 33}
]
```

```diff_elixir:使い方
iex> users3 |> Enum.filter(& elem(&1, 0) == :id001)
[id001: %{name: "foo", weight: nil}]

iex> users3 |> Enum.map(fn {_, v} -> if String.contains?(v.name, "f"), do: String.replace(v.name, "o", ""), else: v.name end)
["hoge", "f", "fuga"]

iex> update_in(users3[:id001].name, &(if String.contains?(&1, "f"), do: String.replace(&1, "o", ""), else: &1))
[
  id000: %{name: "hoge", weight: 49},
  id001: %{name: "f", weight: nil},
  id002: %{name: "fuga", weight: 33}
]
```

## マップキーワードリストからマップリストに戻す

```diff_elixir
users2
|> Enum.map(fn {k, v} -> Map.put(v, :id, k) end)
[
+ %{id: :id000, name: "hoge", weight: 49},
+ %{id: :id001, name: "foo", weight: nil},
+ %{id: :id002, name: "fuga", weight: 33}
]
```

## 配列的アクセスを叶える

```diff_elixir
users4 = 
users
|> Enum.with_index
[
  {%{id: :id000, name: "hoge", weight: 49}, 0},
  {%{id: :id001, name: "foo", weight: nil}, 1},
  {%{id: :id002, name: "fuga", weight: 33}, 2}
]
```

```diff_elixir:使い方
iex> users4 |> Enum.filter(& elem(&1, 1) == 2)
[{%{id: :id002, name: "fuga", weight: 33}, 2}]
```

ここから、もう一段、使いやすくこともできます

```diff_elixir
users5 = 
users 
|> Enum.with_index 
|> Enum.reduce(%{}, & Map.put(&2, elem(&1, 1), elem(&1, 0))) 
%{
  0 => %{id: :id000, name: "hoge", weight: 49},
  1 => %{id: :id001, name: "foo", weight: nil},
  2 => %{id: :id002, name: "fuga", weight: 33}
}
```

```diff_elixir:使い方
iex> users5[1]
%{id: :id001, name: "foo", weight: nil}
```

# マップリストの更新

## Enum.map ＋ if ＋ Map.put

マップキーワードリストよりは、かなりシンプルです

```diff_elixir
users
|> Enum.map(fn v -> if v.id == :id000, do: Map.put(v, :name, "piacere"), else: v end)
|> Enum.map(fn v -> if v.id == :id001, do: v |> Map.put(:name, "fuchan") |> Map.put(:weight, 16), else: v end)
[
+ %{id: :id_000, name: "piacere", weight: 49}, 
+ %{id: :id_001, name: "fuchan",  weight: 16}, 
+ %{id: :id_002, name: "fuga", weight: 35}
]
```

`&` 表記を使うと、更にシンプルになります

```diff_elixir
users
|> Enum.map(& if &1.id == :id000, do: Map.put(&1, :name, "piacere"), else: &1)
|> Enum.map(& if &1.id == :id001, do: &1 |> Map.put(:name, "fuchan") |> Map.put(:weight, 16), else: &1)
```

なお、下記のようにマップキーワードリストだと、更新時の各データ記述が少し複雑なので、更新の都合を考えるとマップリストに優位性があります

```diff_elixir
[
  id000: %{name: "hoge", weight: 60}, 
  id001: %{name: "foo",  weight: nil}, 
  id002: %{name: "fuga", weight: 49}
]
|> Enum.map(fn {k, v} -> if k == :id000, do: {k, Map.put(v, :name, "piacere")}, else: {k, v} end)
|> Enum.map(fn {k, v} -> if k == :id001, do: {k, v |> Map.put(:name, "fuchan") |> Map.put(:weight, 55)}, else: {k, v} end)
[
+ id000: %{name: "piacere", weight: 60},
+ id001: %{name: "fuchan", weight: 55},
+ id002: %{name: "fuga", weight: 49}
]
```

## for ＋ if ＋ Map.put

forはパイプが使えなくて、一時変数が必要なため面倒です

```diff_elixir
for v <- users do
   v2 = if v.id == :id000, do: Map.put(v, :name, "piacere"), else: v
   if v2.id == :id001, do: v2 |> Map.put(:name, "fuchan") |> Map.put(:weight, 16), else: v2
end
[
  %{id: :id000, name: "piacere", weight: 49},
  %{id: :id001, name: "fuchan", weight: 16},
  %{id: :id002, name: "fuga", weight: 33}
]
```

## 一度キーワードリストに変換してput_inし、戻す

`put_in` を知っていれば、こういうテクニックもあります

```diff_elixir
users_k1 = users |> Enum.map(& {&1.id, Map.drop(&1, [:id])})
users_k2 = put_in(users_k1[:id000].name, "piacere")
put_in(users_k2[:id001], %{name: "fuchan", weight: 16})
|> Enum.map(fn {k, v} -> Map.put(v, :id, k) end)
```