LoginSignup
4
0

More than 3 years have passed since last update.

ElixirでJSONPathライクに階層の深い構造から値を取り出す

Last updated at Posted at 2019-05-12

はじめに

とある事情で、階層の深ーいデータ構造を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/2Access.at/1の併用が便利

メモ

remapの実装が、Elixir初級者の私にはとても参考になりました。自分でJSONPathライブラリ書いてみようかな。。

4
0
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
4
0