気になっていた 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_code
が 200
にマッチしたレスポンスを処理しているが、もし status_code
が 200
以外の場合を考慮した場合にはどのように書けば良いのか。
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
以下のような処理が追加されたことになる。
- パイプ演算子として
[]
は空のリストを引数としてtitle_list()
関数を呼び出す - JSON 解析した Body リストの先頭を
head
それ以降をtail
で処理出来るようにする head
で取得した Body リストからtitle
キーをパターンマッチで抽出してjson_title
に保持するjson_title
を[]
で囲むことでリスト化し、titles
に連結してadded_titles
に保持するtail
に保持しているデータに対して 2 〜 4 の処理を繰り返し呼び出す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
パイプ演算子
って便利だなと思ったけど、エラーハンドリングの方法が見つけられずに悩んでいるところ。
参考
- https://www.slideshare.net/piacere_ex/elixir1json-75571537
- https://ja.wikipedia.org/wiki/Elixir_(プログラミング言語)
- http://elixir-lang.org/install.html#unix-and-unix-like
- http://k-shogo.github.io/article/2015/01/05/lets_start_elixir/
- http://qiita.com/HirofumiTamori/items/0dfdbada30c7d8f183fd
- http://qiita.com/HirofumiTamori/items/b72ad232dc3b26e87a03