2017/07/12
(car (cdr ファンクション倶楽部)) LTエディション
やや関数型を意識した風
Elixir/Phoenixご紹介
1
そもそもElixirって
どんな言語?
2
1.Elixirは「データ変換」の言語
3
1.Elixirは「データ変換」の言語
Elixirは、CSVファイルやDBデータ、JSON、バイナリデータ等の
データを受け取り、パターンマッチし、加工・変換するのに適した、
動的型付けの関数型プログラミング言語です
上記の様々なデータフォーマットを、数値や文字列、リスト、マップ、
タプルに展開し、「パターンマッチ」でシンプルにコーディングできます
なお、扱うデータは、全てイミュータブルです
# iex
iex> 1+2
3
iex> list = [ 123, "abc", 456, true ]
[123, "abc", 456, true]
iex> map = %{ "key2" => "abc", "key1" => 123 }
%{"key1" => 123, "key2" => "abc"}
リストは[]で囲む マップは%{}で囲む
2種類書き方がある
①%{k1: "v1"…}
②%{"k1" => "v1"…}
REPLでElixirコードを試せる
4
1.Elixirは「データ変換」の言語:Enum
Enumモジュールで、リスト/マップ/タプルが操作できます
mapやfilter、reduce等、一通りあります
iex> Enum.sort( list )
[123, 456, true, "abc"]
iex> Enum.sort( map )
[{"key1", 123}, {"key2", "abc"}]
マップをソートするとタプルに変換される
(マップは順序を持てないため)
iex> Enum.map( list, fn( x ) -> IO.puts( "value=#{x}" ) end )
value=123
value=abc
value=456
value=true
iex> Enum.filter( list, fn( x ) -> x >= 124 end )
["abc", 456, true]
iex> Enum.reduce( list, fn( x, pre ) -> "#{x}, #{pre}" end )
"true, 456, abc, 123"
5
1.Elixirは「データ変換」の言語:パイプ
パイプを使い、シェルのように、データを前から後ろに繋いで処理
できます (パイプ後の関数の第1引数に前の結果が渡される)
この例はカンタン過ぎて、パイプの恩恵がイマイチ分かりにくいので、
中盤以降のコード例で、改めて紹介します
# iex
iex> list = [ 123, "abc", 456, true ]
iex> List.last( Enum.sort( list ) )
"abc"
iex> list |> Enum.sort |> List.last
"abc"
6
1.Elixirは「データ変換」の言語:パターンマッチ
パターンマッチは、Elixirの最もパワフルな一面です
プロジェクト配下のファイルを以下のように編集して、実行します
マップの”k2”キーの値抽出を、引数だけで実装できてしまいます
# mix new misc
# cd misc
lib/misc.ex
mixというビルドツールでプロジェクトが作成できます
defmodule Misc do
def match_sample( %{ k2: value } ), do: value
end
# iex -S mix
iex> Misc.match_sample( %{ k1: "v1", k2: "v2", k3: "v3" } )
"v2"
iex -S mixでプロジェクトのビルドが走り、iexに入ります
7
1.Elixirは「データ変換」の言語:パターンマッチ
キーだけで無く、キーと値の組み合わせでのマッチも可能です
各関数が、値に応じて、呼び分けされます
他の言語なら、関数呼び出し後にif文で分岐するような処理が、
引数でのパターンマッチで分岐できます
defmodule Misc do
def match( %{ Yes: "we can" } ), do: "Barack Obama"
def match( %{ Yes: need } ), do: need
…
iex> recompile()
iex> Misc.match( %{ No: "-", Yes: "we can", NA: "N/A" } )
"Barack Obama"
iex> Misc.match( %{ No: "-", Yes: "we do", NA: "N/A" } )
"we do"
iex内でリビルドするときはrecompile()
8
1.Elixirは「データ変換」の言語:パターンマッチ
該当キーがマップ内に存在しない場合、マッチエラーになります
引数に”_”を指定すると、その他としてマッチできるようになります
defmodule Misc do
def match( %{ Yes: "we can" } ), do: "Barack Obama"
def match( %{ Yes: need } ), do: need
def match( _ ), do: "Yes...NOT EXIST"
…
iex> Misc.match( %{ No: "-", NA: "N/A" } )
** (FunctionClauseError) no function clause matching in
Crawl.match/1
(crawl) lib/Qiita.ex:5: Crawl.match(%{NA: "N/A", No: "-"})
iex> recompile()
iex> Misc.match( %{ No: "-", NA: "N/A" } )
"Yes...NOT EXIST"
9
1.Elixirは「データ変換」の言語:パターンマッチ
もし、関数内でマッチさせるなら、以下のような書き方になります
(ただ、関数の引数でマッチする書き方の方が推奨です)
実行結果は、引数パターンマッチ版と同じです
defmodule Misc do
def match_in( input_map ) do
%{ Yes: need } = input_map
need
end
…
iex> recompile()
iex> Misc.match_in( %{ No: "-", Yes: "we can", NA: "N/A" } )
"we can"
10
1.Elixirは「データ変換」の言語:パターンマッチ
リストは、パターンマッチにより、「先頭」と「以降全て」で分割でき、
リスト要素の順次処理を組む土台となります
iex> [ head1 | tail1 ] = [ 5, 8, 3, 1 ]
[5, 8, 3, 1]
iex> head1
5
iex> tail1
[8, 3, 1]
iex> [ head2 | tail2 ] = tail1
[8, 3, 1]
iex> head2
8
iex> tail2
[3, 1]
…
iex> head4
[1]
iex> tail4
[]
11
1.Elixirは「データ変換」の言語:パターンマッチ
バイナリデータの分割/マップ化も、パターンマッチでとてもカンタン
に書けます
defmodule MP3 do
defstruct [ :header, :frames, :other_data ]
def analisis(
<<
tag :: bitstring-size( 24 ),
version :: bitstrint-size( 16 ),
flg :: bitstring-size( 8 ),
size :: unsigned-integer-size( 32 ),
data :: binary, # 以降のバイナリデータはここに集約される
>>
) do
%MP3
{
:header MP3Header( tag, version, flg, size )
}
end
end
12
1.Elixirは「データ変換」の言語:再帰(オマケ)
ループは再帰で書きます (末尾再帰は最適化される)
処理対象が残っている間、繰り返される関数と、処理対象が無
くなった際の完了処理の2つの関数を用意し、パターンマッチに
よって呼び分けが行われます
なお、Enumとパターンマッチで大抵の事は済むので、再帰を書く
機会は結構少ないです
defmodule Misc do
def nop( [ head | tail ], rows ), do: nop( tail, rows ++ [ head ] )
def nop( [], rows ), do: rows
…
# iex -S mix
iex> Misc.nop( [ 5, 8, 3, 1 ], [] )
[5, 8, 3, 1]
※一応、for構文はあるけど使う場面はほぼ無い
iex -S mixでプロジェクトのビルドが走り、iexに入ります
13
1.Elixirは「データ変換」の言語:高階関数
高階関数は、3種類の書き方があります
「&」と「&n」で、「fn()」と「end」は省略可能です
関数後の「/n」で、「&n」すらも省略可能です
iex> list = [ 123, "abc", true ]
iex> Enum.map( list, fn( x ) -> IO.puts( "#{x}" ) end )
123
abc
true
iex> Enum.map( list, &IO.puts( &1 ) )
iex> Enum.map( list, &IO.puts/1 )
14
2.【例】JSONパーサを作る
15
Elixirのアプリケーション例として、Qiita API呼出と、返ってくる
JSONのパースを行い、記事タイトルをリストアップしてみます
2.【例】JSONパーサを作る
16
2.【例】JSONパーサを作る
まず、ElixirのHTTPクライアント「HTTPoison」と、JSONパーサ
「Poison」を導入します
各モジュールは、ネットからインストールできます
defmodule Crawl.Mixfile do
…
defp deps do
[
{ :httpoison, "~> 0.7.2" },
{ :poison, "~> 1.5" }
]
end
end
# mix deps.get
mix.exs
17
Qiita APIを呼び出します
実行すると、Qiitaから取得したJSONが出力されます
(Body部以外に、StatusCode等も出力されます)
defmodule Crawl do
def get() do
HTTPoison.get!( "https://qiita.com/api/v2/items?query=Elixir" )
end
end
# iex –S mix
iex> Crawl.get
%HTTPoison.Response{body: "[{"rendered_body":"nu003ch2u003en
…
,"title":"AWS EC2にElixir/Phoenixをインストール","updated_at":"2017-02-10T13:08:41+09:00",
…(他記事が複数並ぶ)…
"twitter_screen_name":null,"website_url":null}}]",
…(Body部以外のHeader等が複数並ぶ)…
{"Content-Length", "4293"}, {"Connection", "keep-alive"}], status_code: 200}
2.【例】JSONパーサを作る
lib/crawl.ex
18
2.【例】JSONパーサを作る
StatusCodeが「200」のときだけ、Body部を抜き出します
(この後、パターンマッチしやすいよう、Poisonでリストマップへの
データ変換も行います)
Body部だけが出力されます
defmodule Crawl 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
iex> recompile()
iex> Crawl.get
[%{"body" => "# ElasticBeanstalk Custom Platformとは?nn今までのElasticBeanstalkでは事前定義済み
…
"title" => "AWS EC2にElixir/Phoenixをインストール",
…(他記事が複数並ぶ)…
"twitter_screen_name" => nil, "website_url" => nil}}]
19
作ったget()を見ると、変数で受け渡しがまどろっこしいです
一方、変数を無くすと、処理順と逆になるため、読みにくいです
defmodule Crawl do
def get() do
response = HTTPoison.get!( "https://qiita.com/api/v2/items?query=
Elixir" )
body = body( response )
Poison.decode!( body )
end
…
2.【例】JSONパーサを作る:パイプで書く
defmodule Crawl do
def get() do
Poison.decode!( body( HTTPoison.get!( "https://qiita.com/api/v2/items?
query=Elixir" ) ) )
…
20
パイプで書くと、直感的かつスマートなコードとなり、ステキです
defmodule Crawl do
def get() do
"https://qiita.com/api/v2/items?query=Elixir"
|> HTTPoison.get!
|> body
|> Poison.decode!
end
…
2.【例】JSONパーサを作る:パイプで書く
21
タイトル抽出を追加し、URL周りをキレイにしたら完成です
こんな感じで、たった11行のコードで、「Qiita APIを呼び、記事
タイトルをリストアップ」するアプリケーションが書けました
2.【例】JSONパーサを作る
defmodule Crawl do
def get( query  "Elixir" ) do
url( query )
|> HTTPoison.get!
|> body
|> Poison.decode!
|> Enum.map( fn( %{ title: title } ) -> title end )
end
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> Crawl.get
["Advent Calendar 2016 投稿数の多いユーザランキング(上位50位)",
"スタートアップ企業の技術選定でPhoenix(Elixir)&React(JS)+時々Echo(go)を採用した",
"PhoenixでAPIによるログイン機能を実装する",
…(他記事が複数並ぶ)…
"Elixir/Phoenix環境をElasticBeanstalk Custom Platformで構築してみた"]
22
コードを改めて見ると、メインの処理は、以下の流れを書き下した
だけの形となっていて、直感的で分かりやすいコードとなっています
URL
→API呼出でJSON取得
→Body抽出
→JSONデコード
→タイトル抽出
2.【例】JSONパーサを作る
defmodule Crawl do
def get( query  "Elixir" ) do
url( query )
|> HTTPoison.get!
|> body
|> Poison.decode!
|> Enum.map( fn( %{ title: title } ) -> title end )
end
def url( query ), do: "https://qiita.com/api/v2/items?query=#{query}"
def body( %{ status_code: 200, body: json_body } ), do: json_body
end
23
3.応用:Qiita API以外のJSONパース
24
3.応用:Qiita API以外のJSONパース
Qiita API以外のAPIも、ほぼ同じコードでJSONパースできます
「はてなAPI」のJSONは、若干壊れている (笑) ので、デコード
前に文字列操作で補正をかけるパイプを追加します
defmodule Hatena do
def get( query  "Elixir" ) do
url( query )
|> HTTPoison.get!
|> body
|> String.slice( 1..-3 ) # そのままだとinvalidなJSONなので加工
|> Poison.decode!
|> Enum.map( fn( %{ title: title } ) -> title end )
end
def url( query ), do: "http://b.hatena.ne.jp/entrylist/json?sort=count&url=#{query}"
def body( %{ status_code: 200, body: json_body } ), do: json_body
end
# iex -S mix
iex> Hatena.get
["【翻訳】超高速なJSON APIをElixirフレームワークのPhoenixでビ...",
"2015/08/22/ElixirとPhoenixとMithrilのFFスタックでChatアプリを作っ...",
…(他記事が複数並ぶ)…
" Elixir ご紹介 // Speaker Deck"]
lib/hatena.ex
25
3.応用:Qiita API以外のJSONパース
「Wikipedia API」のJSONは、Qiita APIより2段、深い階層
構造なので、titleを抽出できる階層まで下るパイプを追加します
defmodule Wikipedia do
def get( query  "Elixir" ) do
url( query )
|> HTTPoison.get!
|> body
|> Poison.decode!
|> dig
|> Enum.map( fn( %{ title: title } ) -> title end )
end
def url( query ), do: "https://ja.wikipedia.org/w/api.php?format=json&action=query
&list=search&srsearch=#{query}"
def dig( %{ "query" => %{ "search" => search } } ), do: search
def body( %{ status_code: 200, body: json_body } ), do: json_body
end
# iex -S mix
iex> Wikipedia.get
["熊沢千絵",
"プログラミング言語一覧",
…(他記事が複数並ぶ)…
"エリクサー"]
lib/wikipedia.ex
26
3.応用:Qiita API以外のJSONパース
「Github API」は、検索パラメータの指定が異なるので、検索パ
ラメータに渡すデフォルト値を変えます
defmodule Github do
def get( query  "Elixir-lang/Elixir" ) do # 例.rails/rails
url( query )
|> HTTPoison.get!
|> body
|> Poison.decode!
|> Enum.map( fn( %{ title: title } ) -> title end )
end
def url( query ), do: "https://api.github.com/repos/#{query}/issues"
def body( %{ status_code: 200, body: json_body } ), do: json_body
end
# iex -S mix
iex> Wikipedia.get
["Use @optional_callbacks in GenServer",
"Improve no function clause error messages",
…(他記事が複数並ぶ)…
"Make Mix more friendly for newcomers"]
lib/github.ex
27
4.高速WebFW「Phoenix」
28
4.高速WebFW「Phoenix」
Elixirで組まれたWebFW「Phoenix」は、関数型でありながら、
Webページでのステートフルも持ち、Web+DBやJSON APIを
コマンド5発で構築でき、高い性能と並行分散も備えています
0
10000
20000
30000
40000
50000
60000
Throughput (req/s)
https://github.com/mroth/phoenix-showdown
http://postd.cc/websocket-shootout
Webページビュー性能比
WebSocket性能比
こんなWeb+DBのCRUD
アプリが、テーブル構築も含め、
コマンド5発で生成できてしまう
29
4.高速WebFW「Phoenix」
Phoenixは、Railsに似たビルドツールによる、Web (+DB)や
JSON API (+DB) の手軽な自動生成を実現しています
(RailsのコミッターがElixirの世界に参入し、RailsライクなPhoenixをコミットしている背景があります)
このコマンド5発で、Web+DBが、テーブルの構築も含め、実現
されます
JSON API+DBの場合は、上記コマンド中、下記1行が異なる
だけで、Web+DBとほぼ変わらない手順でJSON APIが構築
できます
# mix phoenix.new web --no-brunch
# cd web
# mix ecto.create
# mix phoenix.gen.html Post posts title:string body:text
# mix ecto.migrate
# iex -S mix phoenix.server
DB追加
DBに「title」「body」列を持つテーブルを定義
(同時にModelやController、Viewも定義)
DBへのテーブル構築とMVCモジュール生成
# mix phoenix.gen.json Post posts title:string body:text
30
4.高速WebFW「Phoenix」
更に、Elixir構文のAltJS「ElixirScript」や、Haskellライクな
関数型AltJS「Elm」と組み合わせ、フロントサイドもサーバサイド
も関数型なスタックを構成できます (ビルドツールも標準装備)
Model
Model
Model
関数型でMVCライクな「Elmアーキテクチャ」
31
5.副作用の分離
32
UIやステート、DBといった副作用は、サーバプロセスとして分離
し、メッセージパッシングでget/setします
「生きているプロセス」に、値保持やIO処理を閉じ込め、ロック無
通信で高速にやり取りするイメージです
たとえば、IO.puts()での画面表示は、「IOサーバプロセス」への
メッセージパッシングが裏では走っています
5.副作用の分離
iex> IO.puts( "hoge" )
メッセージパッシング
IO処理
IOサーバプロセス
33
ステートは幾つかの実現法ありますが、最もシンプルなパターンの
「Agent」で作る例をQiita人工無能コラム#5で紹介しています
5.副作用の分離
34
よりプリミティブなサーバプロセスとメッセージパッシングを作る例は、
Qiitaに入門スライドへのリンクと軽い解説あるので、ご覧ください
(この続編と、OTP/Redisでの分散・耐障害を今月書きます)
5.副作用の分離

やや関数型を意識した風Elixir/Phoenixご紹介