この記事は、Elixir Advent Calendar 2023 シリーズ10 の20日目です
【本コラムは、10分で読め、10分で試せます】
piacere です、ご覧いただいてありがとございます
下記のようなマップリストは、同列の複数データを扱う際にとても便利で、RepoによるDBデータ取得も基本的にこの形です
users = [
%{id: :id000, name: "hoge", weight: 49},
%{id: :id001, name: "foo", weight: nil},
%{id: :id002, name: "fuga", weight: 33}
]
しかし、ネストした構造のため、中身の更新が面倒な印象があります
また、マップキーワードリストと異なり、1つ前のコラムで示した put_in
も使えません
そこを様々な方法でどうにかしていきたいと思います
マップリストの変形
まず更新を行う前に、基本的な変形から整理します
全項目を2重リスト化
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]
]
使い方
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"]
戻すのもカンタンです
users2
|> Enum.map(& Enum.into(&1, %{}))
[
+ %{id: :id000, name: "hoge", weight: 49},
+ %{id: :id001, name: "foo", weight: nil},
+ %{id: :id002, name: "fuga", weight: 33}
]
項目をマップの外に出すマップキーワードリスト化
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}
]
使い方
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}
]
マップキーワードリストからマップリストに戻す
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}
]
配列的アクセスを叶える
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}
]
使い方
iex> users4 |> Enum.filter(& elem(&1, 1) == 2)
[{%{id: :id002, name: "fuga", weight: 33}, 2}]
ここから、もう一段、使いやすくこともできます
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}
}
使い方
iex> users5[1]
%{id: :id001, name: "foo", weight: nil}
マップリストの更新
Enum.map + if + Map.put
マップキーワードリストよりは、かなりシンプルです
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}
]
&
表記を使うと、更にシンプルになります
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)
なお、下記のようにマップキーワードリストだと、更新時の各データ記述が少し複雑なので、更新の都合を考えるとマップリストに優位性があります
[
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はパイプが使えなくて、一時変数が必要なため面倒です
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
を知っていれば、こういうテクニックもあります
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)