(この記事は Elixir (その2)とPhoenix Advent Calendar 2016 16日目の記事です)
前回、カスタマーサポート向けのツールを作ったりしましたが、その後、「日常業務がプログラミングで無い方向けの、プログラミング教室」的なものを、オフィスのイベントスペースで開催しました
それで、普通のプログラマでも、理解が比較的難しい、「再帰処理によるリストの巡回」をいかにカンタンにできるか、というテーマをクリアするための、小さな関数を作ってみたので、そちらの共有です
ただ、もっとイケてる造りができるかも知れず、「こんなエレガントな書き方あるよ」みたいなご意見あれば、ぜひコメントくださいませ
JSONからtitle項目の値を取りたいけど...
Elixirでは、しょっちゅう再帰処理が出てきます
たとえば、「JSON中にある、全てのtitle項目の値を取る」といったような処理がある場合、まぁ普通に再帰で処理しますよね
以下のコードの、title_list()と_title_list()のところですね
defmodule Crawl do
def get( query \\ "Elixir" ) do
url( query )
|> HTTPoison.get!
|> body
|> Poison.decode!
|> title_list
end
def title_list( body ), do: _title_list( body, [] )
defp _title_list( [ %{ "title" => json_title } | tail ], titles ) do
_title_list( tail, [ json_title | titles ] )
end
defp _title_list( [], titles ), do: titles
def url( query ), do: "https://qiita.com/api/v2/items?query=#{query}"
def body( %{ status_code: 200, body: json_body } ), do: json_body
end
プログラマ向けであっても、このリスト再帰処理を説明した際、スッと入る方もいる一方、かなり細かく動きを追わないとピンと来ない方も多くいました
iexで、以下のような追い方をすれば、比較的、理解が進むとは思いますが、それでも空気を吸うように再帰処理を書けるようになるまで(≒脳の構造を変える、常識が変わる)には、なかなか苦戦するようです
iex> [ head | tail ] = [ "abc", 123, true ]
["abc", 123, true]
iex> head
"abc"
iex> tail
[123, true]
iex> [ head2 | tail2 ] = tail
[123, true]
iex> head2
123
iex> tail2
[true]
iex> [ head3 | tail3 ] = tail2
[true]
iex> head3
true
iex> tail3
[]
慣れたとしても、対象項目や項目数だけが異なる、ソックリさんの関数をちょいちょいコピペする場面は避けたい、と思いますよね?
再帰処理を書かずにJSON項目を抜く関数を作る
Enumモジュールの各関数を普段使っていると、行列操作がとてもエレガントに表現できることに驚きます
なので、この課題も、Enumモジュールの関数のようなノリで解決できないか、と考え始めたのが、きっかけでした
で、色々と考えた結果、以下のような関数を作ってみました
defmodule MapList do
def select( list, key1, key2 \\ "", key3 \\ "", key4 \\ "" ), do: _select( list, [], key1, key2, key3, key4 )
defp _select( [], values, _key1, _key2, _key3, _key4 ), do: Enum.reverse( values )
defp _select( map_list, values, key1, key2, key3, key4 ) do
[ head | tail ] = map_list
value = cond do
key2 == "" ->
%{ ^key1 => value1 } = head
value1
key3 == "" ->
%{ ^key1 => value1, ^key2 => value2 } = head
{ value1, value2 }
key4 == "" ->
%{ ^key1 => value1, ^key2 => value2, ^key3 => value3 } = head
{ value1, value2, value3 }
true ->
%{ ^key1 => value1, ^key2 => value2, ^key3 => value3, ^key4 => value4 } = head
{ value1, value2, value3, value4 }
end
_select( tail, [ value | values ], key1, key2, key3, key4 )
end
…(その他の関数もあるけど今回は割愛)…
end
key1は指定必須ですが、key2~key4は、デフォルト引数でそれぞれ省略可能となっており、指定パターンに応じて、JSON項目から幾つ抜くかの挙動が変わります
各パターンを関数で書き分けることも可能なんですが、再帰呼び出しを毎回書くのがメンドかったので、cond doで条件分岐させちゃいました
(その結果、第1引数に[]を受け取る関数を先に定義しないと、コンパイル時にエラーが出ます)
では、この関数を使って、JSONからtitle項目を抜く処理を書き換えてみましょう
defmodule Crawl do
def get( query \\ "Elixir" ) do
url( query )
|> HTTPoison.get!
|> body
|> Poison.decode!
|> MapList.select( "title" )
end
…
だいぶ直感的になりましたね
再帰処理が、SQLに化けた感じでしょうか?
対象項目は、複数指定できるので、たとえばtitleとbodyを両方抜くときは、こんな感じです(4つまで指定可能)
defmodule Crawl do
def get( query \\ "Elixir" ) do
url( query )
|> HTTPoison.get!
|> body
|> Poison.decode!
|> MapList.select( "title", "body" )
end
…
もっとも、Enum.map()等に慣れたら、上記より少し複雑だけど、ほぼ似たような書き方ができるようになるので、関数は不要になりますが、これはまた次回に