はじめに
とある事情で、階層の深ーいデータ構造をErlang/Elixirで扱うことになりました。
ここから特定のデータを取り出す方法について、少し調査が必要だったのでまとめます。
サマリ
- 1階層ごと辿るなら
Kernel.get_in/2
が基本-
Access.at/1
を使うと途中にlistがあっても無問題
-
- JSONPathの記法を使いたい場合や、途中階層を飛ばして孫要素以降を取り出したい場合のためにはGithubやHexにいくつかのライブラリが見つかったが、いずれも機能は限定的
やりたいこと
https://hexdocs.pm/elixir/Access.html#module-nested-data-structures に倣って、次のようなデータ構造を例に挙げます。
user = %{
name: "john",
languages: [
%{name: "elixir", type: :functional},
%{name: "C", type: :procedural}
]
}
例えば深いJSONをパースするとこのようなデータ構造になると思います。
特徴としては、下記のようなものが挙げられます。
- 1つのルート要素が複数の子要素を持ちうる
- そのまた子要素もありうる
- 親要素はmapかもしれないし、listかもしれない
ここから特定のデータ、例えばJSONPathでいうlanguages[0].name
を取り出すことを考えます。
パイプラインを用いる
まず思いつくのがこの方法でした。
iex(3)> user |>
...(3)> Map.get("languages") |>
...(3)> Enum.at(0) |>
...(3)> Map.get("name")
"elixir"
ただし、この場合は存在しないkeyやindexを指定すると怒られるので、指定しうる場合は気をつける必要がありそうです。
iex(4)> non_programmer = %{
...(4)> "name" => "jane",
...(4)> "languages" => []
...(4)> }
iex(6)> non_programmer |>
...(6)> Map.get("languages") |>
...(6)> Enum.at(0) |>
...(6)> Map.get("name")
** (BadMapError) expected a map, got: nil
(elixir) lib/map.ex:437: Map.get(nil, "name", nil)
Mapに対しては、Access.get/2
を使えばnil
に対する呼び出しやkeyが存在しない場合にnil
を返してくれます。
iex(7)> non_programmer |>
...(7)> Access.get("languages") |>
...(7)> Enum.at(0, nil) |>
...(7)> Access.get("name")
nil
Kernel.get_in/2
を使う
公式ドキュメントをみると、
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> get_in(users, ["john", :age])
27
のようなmapのkeyを指定する方法や、
iex> all = fn :get, data, next -> Enum.map(data, next) end
iex> get_in(users, [all, :age])
[27, 23]
のように、keyに関数を渡す方法が記載されていました。
後者の方法を使うとlistのindexの指定もできました。
iex(6)> at_0 = fn :get, data, next -> data |> Enum.at(0) |> next.() end
iex(7)> user |>
...(7)> get_in(["languages", at_0, "name"]) |>
...(7)> IO.inspect(label: "get_in w/ func")
get_in w/ func: "elixir"
"elixir"
また、Access.at/1
を使うと、上記のようなlistの特定要素を取得する関数を生成してくれるので、普段はこれを使うのが良さそうです。
iex(8)> user |> get_in(["languages", Access.at(0), "name"])
"elixir"
外部ライブラリを使う
get_in/2
である程度柔軟に深い階層の値を取り出すことができるようになりました。ただ、心のどこかから
- JSONPathのように簡潔な記述で指定したい。。
- JSONPathの
..
のように、孫要素以降を列挙したい。。
という声が聞こえてきて、外部ライブラリで提供されていないか調べてみました。
のあたりが見つかりましたが、前者は..
に非対応、後者はlistのindex指定に非対応と、ちょっと機能が少ない印象を受けました。
まとめ
Kernal.get_in/2
とAccess.at/1
の併用が便利
メモ
remapの実装が、Elixir初級者の私にはとても参考になりました。自分でJSONPathライブラリ書いてみようかな。。