LoginSignup
11
4

More than 5 years have passed since last update.

再帰処理を毎回書かずにJSON項目を抜く

Last updated at Posted at 2017-06-05

(この記事は Elixir (その2)とPhoenix Advent Calendar 2016 16日目の記事です)

前回、カスタマーサポート向けのツールを作ったりしましたが、その後、「日常業務がプログラミングで無い方向けの、プログラミング教室」的なものを、オフィスのイベントスペースで開催しました

それで、普通のプログラマでも、理解が比較的難しい、「再帰処理によるリストの巡回」をいかにカンタンにできるか、というテーマをクリアするための、小さな関数を作ってみたので、そちらの共有です :sunrise_over_mountains:

ただ、もっとイケてる造りができるかも知れず、「こんなエレガントな書き方あるよ」みたいなご意見あれば、ぜひコメントくださいませ :headphones:

JSONからtitle項目の値を取りたいけど...

Elixirでは、しょっちゅう再帰処理が出てきます

たとえば、「JSON中にある、全てのtitle項目の値を取る」といったような処理がある場合、まぁ普通に再帰で処理しますよね

以下のコードの、title_list()と_title_list()のところですね :kissing:

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
[]

慣れたとしても、対象項目や項目数だけが異なる、ソックリさんの関数をちょいちょいコピペする場面は避けたい、と思いますよね? :muscle_tone1:

再帰処理を書かずに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で条件分岐させちゃいました :sweat_smile:

(その結果、第1引数に[]を受け取る関数を先に定義しないと、コンパイル時にエラーが出ます)

では、この関数を使って、JSONからtitle項目を抜く処理を書き換えてみましょう

defmodule Crawl do
    def get( query \\ "Elixir" ) do
        url( query )
        |> HTTPoison.get!
        |> body
        |> Poison.decode!
        |> MapList.select( "title" )
    end
 …

だいぶ直感的になりましたね

再帰処理が、SQLに化けた感じでしょうか? :relaxed:

対象項目は、複数指定できるので、たとえば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()等に慣れたら、上記より少し複雑だけど、ほぼ似たような書き方ができるようになるので、関数は不要になりますが、これはまた次回:wink:

11
4
7

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