Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
4
Help us understand the problem. What is going on with this article?
@piacerex

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

More than 3 years have passed since last update.

(この記事は 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:

4
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
piacerex
福岡でプログラマしながらIT商社とIT企業を経営してます。Elixir/Kerasをよく使う。Elixirコミュ#fukuokaex、福岡理学部#FukuokaScienceを主催。プログラマ歴36年/XPer歴19年/デジタルマーケッター/経営者/CTO/技術顧問数社。 シボと重力子放射線射出装置は別腹(^^)
fukuokaex
エンジニア/企業向けにElixirプロダクト開発・SI案件開発を支援する福岡のコミュニティ

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
4
Help us understand the problem. What is going on with this article?