読者です 読者をやめる 読者になる 読者になる

mmag

ハマったことメモなど

get_in/2 と Access.all/0 の処理を追っていく

昨日はAccessモジュールに入った便利関数についてのエントリを書きました。

今日はそれがどうやって実現されているのか読みます。今の時点での最新版はd24a263です。

Access.all/0

まずそもそもAccess.all()は何を返すのでしょうか。

def all() do
  &all/3
end

Access.all()は関数を返すようです。ではAccess.all/3はどんな関数なのでしょう。

defp all(:get, data, next) when is_list(data) do
  Enum.map(data, next)
end

defp all(:get_and_update, data, next) when is_list(data) do
  all(data, next, [], [])
end

defp all(_op, data, _next) do
  raise "Access.all/0 expected a list, got: #{inspect data}"
end

defp all([head | rest], next, gets, updates) do
  case next.(head) do
    {get, update} -> all(rest, next, [get | gets], [update | updates])
    :pop -> all(rest, next, [head | gets], updates)
  end
end

defp all([], _next, gets, updates) do
  {:lists.reverse(gets), :lists.reverse(updates)}
end

よくわかんないのでKernelモジュールのget_in/2を見てみましょう。

get_in/2

def get_in(data, [h]) when is_function(h),
  do: h.(:get, data, &(&1))
def get_in(data, [h | t]) when is_function(h),
  do: h.(:get, data, &get_in(&1, t))

def get_in(nil, [_]),
  do: nil
def get_in(nil, [_ | t]),
  do: get_in(nil, t)

def get_in(data, [h]),
  do: Access.get(data, h)
def get_in(data, [h | t]),
  do: get_in(Access.get(data, h), t)

よくわかんないので実際にget_in/2を使ったときにどう処理が進むか見てみましょう。

map = %{users: [%{name: "john"}, %{name: "mary"}]}
get_in(map, [:users, Access.all, :name])

を考えてみましょう。最終的な戻り値は["john", "mary"]になるはずです。

まずは

def get_in(data, [h | t]),
  do: get_in(Access.get(data, h), t)

なので、

get_in(Access.get(map, :users), [Access.all, :name])

です。Access.get/2は第2引数をキーとして第1引数からいい感じにデータを取得します。雑に言うとDict.get/2みたいなもんです。ということは、

get_in([%{name: "john"}, %{name: "mary"}], [Access.all, :name])

こう。第2引数の先頭がAccess.allなので、↓の関数にマッチします。

def get_in(data, [h | t]) when is_function(h),
  do: h.(:get, data, &get_in(&1, t))

一瞬なにやってんだ と思いましたが落ち着きましょう。これはつまりこういうこと。

all(:get, [%{name: "john"}, %{name: "mary"}], &get_in(&1, [:name]))

all/3もいくつかパターンがあるのは既に見ました。マッチするのはこれ。

defp all(:get, data, next) when is_list(data) do
  Enum.map(data, next)
end

よって

Enum.map([%{name: "john"}, %{name: "mary"}], &get_in(&1, [:name]))

これは簡単。mapしてるだけ。以下と等価。

[get_in(%{name: "john"}, [:name]), get_in(%{name: "mary"}, [:name])]

つまり結果は

["john", "mary"]

なんとか答えにたどり着きました。ふう、疲れた、寝たい、のですが1ヶ所気になったコードがあったのでそこだけ最後に。

def get_in(nil, [_]),
  do: nil
def get_in(nil, [_ | t]),
  do: get_in(nil, t)

これ。get_in(nil, [_ | t])get_in(nil, t)を返しています。つまりnilから何を取ろうとしてもnilということ。

get_in(nil, [:a, :b, :c, :d])  #=> nil

get_inしている途中でnilになってしまったら、戻り値は必ずnilということ。

get_in(%{e: 1}, [:a, :b, :c, :d])  #=> nil

これは覚えておいたほうがいいかもですね。

テーマはFB matteをベースにしてます。作者さんに感謝を込めて。