ようへいの日々精進XP

よかろうもん

ゴールデンウィークスペシャル:Elixir チュートリアル (2) 〜 JSON パース編〜

気になっていた Elixir という言語について、以下の資料を写経して引き続きチュートリアルしてみた。

www.slideshare.net

チュートリアル

チュートリアル環境

  • OS は Ubuntu Xenial でお届けします
ubuntu@ubuntu-xenial:~$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
  • Elixir は 1.4.1 でお届けします
ubuntu@ubuntu-xenial:~$ elixir --version
Erlang/OTP 19 [erts-8.3] [source-d5c06c6] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Elixir 1.4.1

JSON パースに必要なライブラリの導入

導入するライブラリ

以下のライブラリを導入する。

ライブラリ名 Github リポジトリ 用途
HTTPoison https://github.com/edgurgel/httpoison HTTP クライアント
Poison https://github.com/devinus/poison JSON パーサー

mix.exs を修正

sample/mix.exs を以下のように修正する。

--- mix.exs.original    2017-05-05 02:36:31.000000000 +0000
+++ mix.exs     2017-05-05 02:37:13.000000000 +0000
@@ -28,6 +28,9 @@
   #
   # Type "mix help deps" for more examples and options
   defp deps do
-    []
+    [
+      { :httpoison, "~> 0.7.2" },
+      { :poison, "~> 1.5" }
+    ]
   end
 end

Ruby だと Gemfile みたいなものですな、きっと。

以下を実行して各モジュールを取得する。

ubuntu@ubuntu-xenial:~$ mix deps.get

以下のように出力される。

Running dependency resolution...
Dependency resolution completed:
  hackney 1.3.2
  httpoison 0.7.5
  idna 1.0.3
  poison 1.5.2
  ssl_verify_hostname 1.0.6
* Getting httpoison (Hex package)
  Checking package (https://repo.hex.pm/tarballs/httpoison-0.7.5.tar)
  Using locally cached package
* Getting poison (Hex package)
  Checking package (https://repo.hex.pm/tarballs/poison-1.5.2.tar)
  Using locally cached package
* Getting hackney (Hex package)
  Checking package (https://repo.hex.pm/tarballs/hackney-1.3.2.tar)
  Using locally cached package
* Getting idna (Hex package)
  Checking package (https://repo.hex.pm/tarballs/idna-1.0.3.tar)
  Using locally cached package
* Getting ssl_verify_hostname (Hex package)
  Checking package (https://repo.hex.pm/tarballs/ssl_verify_hostname-1.0.6.tar)
  Using locally cached package

Ruby だと bundle install みたいなものですが、きっと。

Qiita API を呼び出す

Qiita API を呼び出すコードを書く

以下のように get() 関数を追加する。

defmodule Sample do
  ...
  def get() do
    HTTPoison.get!("https://qiita.com/api/v2/items?query=Elixir")
  end
end

Sample 関数を recompile() した後に Sample 関数を実行すると以下のように Qiita API から Elixr 関連記事を取得する。

iex(5)> Sample.get
...

{"X-Request-Id", "850ad9f5-9b62-4a65-8ffb-d1f37ffb511d"},
{"X-Runtime", "0.432123"}, {"X-XSS-Protection", "1; mode=block"},
{"transfer-encoding", "chunked"}, {"Connection", "keep-alive"}],
status_code: 200}
iex(6)>

おお、簡単ばい。

Body だけを出力したい

以下のように get() 関数を修正して Body だけを抜き出す。

defmodule Sample do
  def get() do
    response = HTTPoison.get!("https://qiita.com/api/v2/items?query=Elixir")
    body = body( response )
    Poison.decode!( body )
  end
  def body( %{ status_code: 200, body: json_body } ), do: json_body
end

改めて recompile() を実行して Sample 関数を実行すると Qiita API レスポンスから Body のみを取得出来る。

iex(17)> Sample.get
...

"user" => %{"description" => "", "facebook_id" => "", "followees_count" => 0,
  "followers_count" => 1, "github_login_name" => nil, "id" => "mdmom",
  "items_count" => 5, "linkedin_id" => "", "location" => "", "name" => "",
  "organization" => "", "permanent_id" => 152421,
  "profile_image_url" => "https://avatars.githubusercontent.com/u/18415389?v=3",
  "twitter_screen_name" => nil, "website_url" => ""}}]

おお。いい感じ。

ちなみに

status_code200 にマッチしたレスポンスを処理しているが、もし status_code200 以外の場合を考慮した場合にはどのように書けば良いのか。

defmodule Sample do
  def get() do
    response = HTTPoison.get!("https://qiita.com/api/v3/items?query=Elixir")
    if response.status_code == 200 do
      body = body( response )
      Poison.decode!( body )
    else
      response.status_code
    end
  end
  def body( %{ status_code: 200, body: json_body } ), do: json_body
end

こんな感じかな…おお、Ruby となんか似てる。こちらによると、cond do ~ という書き方もあるらしい。

defmodule Sample do
  def get() do
    response = HTTPoison.get!("https://qiita.com/api/v3/items?query=Elixir")
    cond do
      response.status_code == 200 ->
        body = body( response )
        Poison.decode!( body )
      response.status_code != 200 ->
        response.status_code
    end
  end
  def body( %{ status_code: 200, body: json_body } ), do: json_body
end

パイプ演算子

変数の受け渡しがちょっと煩わしい時がある

現状は以下のように Qiita からデータを取得して Body を抽出し JSON をデコードするという流れを変数で受け渡しを行っている。

defmodule Sample do
  def get() do
    response = HTTPoison.get!("https://qiita.com/api/v2/items?query=Elixir")
    body = body( response )
    Poison.decode!( body )
  end
  def body( %{ status_code: 200, body: json_body } ), do: json_body
end

まあ、別にコレでもいいかなーと思うけど。

パイプ演算子が便利

Elixir ではパイプ演算子を使えばスマートに書くことが出来る。

defmodule Sample do
  def get() do
    HTTPoison.get!("https://qiita.com/api/v2/items?query=Elixir")
    |> body
    |> Poison.decode!
  end
  def body( %{ status_code: 200, body: json_body } ), do: json_body
end

|> は前の処理の出力を次の処理の第一引数として渡すことが出来る。引数が複数ある場合には、第二引数を以降を () で括って指定が可能。

タイトルのみを抽出する

Qiita API のレスポンス Body

レスポンスを解析すると以下のような内容となっている。

[
%{ "body" => "body1", ..., "title" => "titile1" },
%{ "body" => "body2", ..., "title" => "titile2" },
%{ "body" => "body3", ..., "title" => "titile3" },
]

記事のタイトルのみを取得する

以下のように (1) 〜 (6) を追加する。

defmodule Sample do
  def get() do
    HTTPoison.get!("https://qiita.com/api/v2/items?query=Elixir")
    |> body
    |> Poison.decode!
    |> title_list( [] )                          # 追加 1
  end
  def body( %{ status_code: 200, body: json_body } ), do: json_body
  def title_list( [ head | tail ], titles ) do   # 追加 2
    %{ "title" => json_title } = head            # 追加 3
    added_titles = [ json_title ] ++ titles      # 追加 4
    title_list( tail, added_titles )             # 追加 5
  end
  def title_list( [], titles ), do: titles       # 追加 6
end

以下のような処理が追加されたことになる。

  1. パイプ演算子として [] は空のリストを引数として title_list() 関数を呼び出す
  2. JSON 解析した Body リストの先頭を head それ以降を tail で処理出来るようにする
  3. head で取得した Body リストから title キーをパターンマッチで抽出して json_title に保持する
  4. json_title[] で囲むことでリスト化し、titles に連結して added_titles に保持する
  5. tail に保持しているデータに対して 2 〜 4 の処理を繰り返し呼び出す
  6. tail が空になった際に呼ばれて、titles を出力する

recompile() を実行した後に関数を実行すると以下のように出力される。

iex(45)> Sample.get
["Amazon EC2上にRed Hatインスタンスを作成してElixir Reportをコンソールインストールする手順",
 "Mastodon を Node.js で遊んでみる", "CowboyのHelloWorldまで",
...
 "Elixirで弱々しいAI#3を作る「MeCab辞書差し替え |> CaboChaモジュールの作成」"]

おお。

例のごとく、Ruby で書くと以下のように書けると思う。

irb(main):001:0> require 'net/http'
=> true
irb(main):002:0> require 'json'
=> true
irb(main):003:0> res = Net::HTTP.get(URI.parse('https://qiita.com/api/v2/items?query=Elixir'))
=> ...
...
irb(main):004:0> titles = []
=> []
irb(main):005:0> JSON.parse(res).each { |body| titles << body['title'] }
irb(main):006:0> titles.reverse
=> ["Amazon EC2上にRed Hatインスタンスを作成してElixir Reportをコンソールインストールする手順", "Mastodon を Node.js で遊んでみる", "CowboyのHelloWorldまで", "[翻訳] Elixirでスモールデータを扱う", "Elixir製のプロジェクトをTravisCIで運用するための.travis.yml", "Amazon EC2上のRed Hat Enterprise LinuxにX11転送でElixir ReportをGUIインストールする手順", "Amazon EC2上のRed Hat Enterprise Linuxにリモートデスクトップ接続してElixir ReportをGUIインストールする手順", "Ectoでバリデーション", "【elixir】slackbotをつくり定期的にコメントさせる", "Laravel5系blade中でassetファイルのlastModified追加", "Ectoでカスタムバリデーションを追加する", "Google Compute EngineのRed Hat VMインスタンス作成と、X11転送でGUIインストーラをWindowsから操作する手順 - Elixir Report", "PhoenixでPlugを使用しアクションの前にフィルターを実装する", "Goの構造体にメ タ情報を付与するタグの基本", "Google Compute EngineでのRed Hat Enterprise Linux VMインスタンス作成と、Elixir Reportのコマンドインストール手順", "ElixirでSleep Sort書いてみた", "Elixirで弱々しいAI#1を作る「MeCabで文章パース」", "Elixirのソースコードにバイナリを埋めこむ方法とそのバイナリ表現の生成方法", "Elixirで弱々しいAI#2を作る「構文解釈してオウム返し(ついでにPhoenixでWebアプリ化)」", "Elixirで弱々しいAI#3を作る「MeCab辞書差し替え |> CaboChaモジュールの作成」"]

ということで

sample.ex

これまでのチュートリアルで作成した sample.ex は以下のようになった。

defmodule Sample do
  # def sort( values ) do
  #   Enum.sort( values )
  # end
  def sort( values ), do: Enum.sort( values )
  def match( %{ Yes: "we can" } ), do: "Barack Obama"
  def match( %{ Yes: need } ), do: need
  def match(_), do: "Yes...Not EXIST"
  def match_inner( input_map ) do
    %{ Yes: need } = input_map
    need
  end
  # def nums( [ head | tail ], rows ), do: nums( tail, [ head | rows ] )
  # def nums( [], rows ), do: rows
  def nums( [ head | tail ], rows ) do #...1
    nums( tail, [ head | rows ] )      #...2
  end
  def nums( [], rows ) do              #...3
    rows                               #...4
  end

  def sum_list( [ head | tail ], total ) do
    sum_list( tail, head + total )
  end
  def sum_list( [], total ) do
    total
  end

  def get() do
    HTTPoison.get!("https://qiita.com/api/v2/items?query=Elixir")
    |> body
    |> Poison.decode!
    |> title_list( [] )
  end
  def body( %{ status_code: 200, body: json_body } ), do: json_body
  def title_list( [ head | tail ], titles ) do
    %{ "title" => json_title } = head
    added_titles = [ json_title ] ++ titles
    title_list( tail, added_titles )
  end
  def title_list( [], titles ), do: titles
end

パイプ演算子

って便利だなと思ったけど、エラーハンドリングの方法が見つけられずに悩んでいるところ。

参考