LoginSignup
6
1
記事投稿キャンペーン 「2024年!初アウトプットをしよう」

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

Last updated at Posted at 2024-01-01

この記事は、Elixir Advent Calendar 2023 シリーズ10 の20日目です


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

piacere です、ご覧いただいてありがとございます :bow:

下記のようなマップリストは、同列の複数データを扱う際にとても便利で、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)
6
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
6
1