11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Elixir (その2)とPhoenixAdvent Calendar 2016

Day 16

再帰処理を毎回書かずに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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?