最近話題のWeb言語 Elixirのご紹介

327件のシェア(そこそこ話題の記事)

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ここ最近、Elixirという言語がWebプログラマー界隈で話題になっています。
ElixirはErlangのVM上で動くRuby風味の関数型言語で、RailsコミッタのJose Valimさんが作ったプログラミング言語です。
elixir-flame

この言語はErlangの並行性とRubyの開発生産性を持っています。
今回はこの言語が出てきた背景と文法の特徴について書こうと思います。

Elixirが注目されている理由

ここ数年、Webアプリケーションは以下の問題を抱えています。

  • C10K問題
    10000以上のクライアントがサーバに同時接続すると、HWの性能は足りていてもOS、ソフトウェアの問題で処理が破綻する
  • ムーアの法則の限界
    CPUチップメーカーの製造するチップのクロックレートの成長に陰り。代わりにマルチコアアーキテクチャのCPUが開発されている
  • リアルタイムWeb
    サーバ、クライアントが常時接続するアプリケーションが増えてきた

これらを解決するために従来のマルチスレッドもしくはイベント駆動のスタイルで開発していたのですが、どちらも様々な問題(実装上の問題)がプログラマを悩ませてきました。
並行処理性能に優れ、モダンなシンタックス、ライブラリを持つプログラミング言語で開発したいという要望がプログラマーの間に強くあり、それがElixirが誕生した背景です。

インストール手順

Mac環境のみの説明ですが、Homebrewを使って簡単にインストールできます。

brew install elixir

rubyのirbのようなREPLもあります。
iexコマンドを使います。

$ iex
iex(1)> IO.puts "hello world"
hello world
:ok

シンタックスの特徴

雰囲気を掴んでもらうためにまずはサンプルコードを書きました。詳細については後から説明していきますので、今は理解しなくて大丈夫です。

defmodule HatenaBookmarks do
  
  # ここがアプリケーションのエントリーポイント
  def fetch_entry(url) do
    HTTPoison.start
    HTTPoison.get!("http://b.hatena.ne.jp/entrylist/json?sort=count&url=" <> url)
    |> decode_response
    |> extract_entry
    |> sort_descending_and_format
  end

  # APIを呼び出した結果をJSONにデコード
  def decode_response(%{status_code: 200, body: body}) do
    String.slice(body, 1..-3)
    |> Poison.decode!
  end

  def decode_response(%{status_code: ___, body: body}) do
    body
    |> Poison.decode!
  end

  # デコードしたJSONからblogエントリーのタイトルとURLを抽出
  def extract_entry(items), do: extract_entry(items, [])
  defp extract_entry([], res), do: res
  defp extract_entry([%{"title" => title, "count" => count}|tail], res) do
    extract_entry(tail, [{title, count} | res])
  end

  # 最後にソートと表示のために文字列を加工します
  def sort_descending_and_format(list_of_entries) do
    list_of_entries
    |> Enum.sort( fn {_, count1}, {_, count2} ->
      String.to_integer(count1) >= String.to_integer(count2) end)
    |> Enum.map( fn ({title, count}) -> "#{title} : #{count}件" end)
  end

  def format_entry(list_of_entries) do
    Enum.map(list_of_entries, fn ({title, count}) -> "#{title} : #{count}件" end)
  end

end

# ここからコードが実行されます
HatenaBookmarks.fetch_entry("https://dev.classmethod.jp")
|> Enum.each fn(entry) ->
  IO.inspect entry
end

どうでしょうか?思ってたよりRubyっぽくないと感じるかもしれません。
このサンプルコードでやっていることは

  1. はてなブックマークAPIでclassmethod開発者Blogのエントリー情報を取得
  2. JSONにデコード
  3. エントリー情報のJSONからタイトルとブックマーク数を抽出
  4. 最後にソート、表示用の文字列整形をして終わり

それでは主な特徴をもう少しシンプルなコードで説明していきます。

パイプライン演算子

まずはElixirの一番の特徴であるパイプライン演算子から

1..10
|> Enum.map(fn(x) -> x * x end)
|> Enum.filter(fn(x) -> x < 40 end)
|> IO.inspect
# => [1,4,9,16,25,36]

この「 |> 」 で関数をつなげると関数の返却値を次の関数の第一引数に渡す動きをします。
Elixirで実装するときは小さな関数を作成しこのパイプライン演算子で関数をつなげることで大きな処理を実装します。

パターンマッチ

Haskellなどの関数型言語ではお馴染みのパターンマッチですが、Elixirでもいろいろなところで使われています。

%{:name => company} = %{:name => "classmethod"}
IO.puts(company)
# => classmethod

簡単な例ですが、右辺のMapのキー値:nameと左辺のMapのキー値:nameが一致した場合に変数companyに"classmethod"の文字列を束縛します(ElixirのMapは%{:key => value}の形式で記述します。

パターンマッチのサンプルをもう一つ、次は再帰と組み合わせます。

defmodule MyList do
  def size([head|tail]), do: 1 + size(tail)
  def size([]), do: 0
end

MyList.size(["Elixir", "Ruby", "JavaScript"])
# => 3

リストの要素数をカウントするサンプルです。size関数を実行すると

変数名
head "Elixir"
tail ["Ruby", "JavaScript"]

のように変数に値が束縛されます。そして1を加算し、tailを引数に指定してsize関数を再帰呼び出しします。
最後に引数のリストが空になったら処理終了です。終了条件としてリストが空の場合のときの処理も記述します。

無限ストリーム

フィボナッチ数列の無限ストリームの例です。
処理が遅延評価されるのでEnum.take(10)を呼び出すまで計算は実行されません。

Stream.unfold({0,1}, fn {f1,f2} -> {f1, {f2, f1+f2} } end)
|> Enum.take(10)
|> IO.inspect
# => [0,1,1,2,3,5,8,13,21,34]

続いてファイルストリームの例です。
Enum.max_by(&String.length/1)を呼び出すまでファイル読み込み処理は実行されません。

IO.puts File.stream!("/usr/share/dict/words")
|> Enum.max_by(&String.length/1)

リスト内包表記

数学の集合で使う内包的記法のようなもので、リストのフィルタリングや変換などに使うものです。
今回のサンプルでは

  • 1から10の数値を二乗して、40以下の値を抽出する

をリスト内包表記で書いてみます。

Haskellでは

[x*x | x <- [1..10], x*x < 40]

と書きますがElixirでは次のように書きます

for x <- 1..10, x*x <40, do x*x

最初のサンプルの説明

ここまでElixirの関数型言語としての機能の説明をしてきました。これで最初のサンプルコードの内容が理解できます。

パイプライン演算子

アプリケーションのエントリポイントの関数fetch_entryです。

def fetch_entry(url) do
    HTTPoison.start
    HTTPoison.get!("http://b.hatena.ne.jp/entrylist/json?sort=count&url=" <> url)
    |> decode_response
    |> extract_entry
    |> sort_descending_and_format
  end

このコードは次の順で処理を実行しています。実行結果はパイプライン演算子を使い次の関数の第一引数に渡しています。

  1. はてなブックマークAPIを叩いてサイトのエントリー情報を取得
  2. 取得結果をJSONにデコード
  3. 変換したJSONからタイトル、ブックマーク数を抽出
  4. 最後にソート(ブックマーク数の多い順)と文字列整形

パターンマッチ

続いてdecode_responseの説明です

def decode_response(%{status_code: 200, body: body}) do
  String.slice(body, 1..-3)
  |> Poison.decode!
end

この関数に渡されるのはAPI呼び出し結果のMap型のデータです(前の処理とパイプライン演算子でつなぐことで実現している)。
そして渡されたデータに対してパターンマッチを使ってstatus_codeの値が200の場合のみ処理が呼び出されデコードします。

なおAPI呼び出し結果のデータは以下です(量が多いので一部のみ記載)。

%HTTPoison.Response{body: "([{\"link\":\"https://dev.classmethod.jp/smartphone/build-fast-android-emulator/\",\"count\":\"1072\",\"title\":\"【番外編】Androidの爆速エミュレータ環境を構築する | ク...\"}, ...]);",
 headers: [{"Server", "nginx"}, {"Date", "Tue, 03 Nov 2015 12:56:38 GMT"},
  {"Content-Type", "application/json; charset=utf-8"},
  {"Content-Length", "1685"}, {"Connection", "keep-alive"}, ...], 
 status_code: 200}

status_codeキーが最後の行にあります。decode_response関数はこのstatus_codeの値を見てbody要素を取り出して関数を実行することができるようになっています。  

パターンマッチ+再帰    

デコードしてできたJSONの配列を受け取るextract_entry関数の説明です。

def extract_entry(items), do: extract_entry(items, [])
defp extract_entry([], res), do: res
defp extract_entry([%{"title" => title, "count" => count}|tail], res) do
  extract_entry(tail, [{title, count} | res])
end

同じ名前の関数がdefとdefpで宣言されているのが気になりますね。defpはElixirでプライベートな関数を宣言するときの記法です。

  1. JSONの配列をパブリック関数のextract_entryが受け取りextract_entry(items, [])を呼び出す。
  2. プライベート関数のextract_entryがJSONの配列と空の配列(res)を受け取ります。
    1. 先頭要素のtitleとcountを変数に束縛、残り要素をtailに束縛します。
    2. titleとcountを空の配列の先頭要素に追加し、extract_entry関数を再帰呼び出しします。
  3. 再帰呼び出しを続け最後にJSON配列が空になったら処理終了です。この終了条件は再帰呼び出しの関数を呼ぶ場合必ず必要です。

なお、以下の記述は配列の先頭に要素を追加するという意味です。

 
["hoge" | [] ] 
# => ["hoge"]

2要素追加する場合はこうなります。

["hoge" | ["fuga" | [] ]]
# => ["hoge, "fuga"]

はてなブックマークAPIのサンプルコードの場合、この処理を繰り返しresという配列に抽出した情報を追加し続けます。

[{title, count} | res] # resの初期値は空の配列

サンプルコードの結果

Elixirと全く関係ないのですが、一応サンプルコードの結果を載せておきます。
AWSとモバイル開発で認識されている会社らしい結果ですね。

"【番外編】Androidの爆速エミュレータ環境を構築する | ク... : 1072件"
"【AWS】非エンジニアのための初めてのAmazon Web Services 資料を... : 973件"
"MacBook Airの動作が重くなったのでメンテナンスしたら軽くな... : 959件"
"よく分かる!iOS アプリのリリース手順のまとめ | Developers.... : 900件"
"これからAWSを始める人は一読すべき「AWS運用チェックリス... : 893件"
"iOS 7 特集 | Developers.IO : 800件"
"ユニットテストにまつわる10の勘違い | Developers.IO : 795件"
"HTML5 × CSS3 × jQueryを真面目に勉強 – #6 パララックスエフェ... : 776件"
"JavaScriptのMVCフレームワークと仲間たち | Classmethod.dev() : 701件"
"Android案件の見積り | Classmethod.dev() : 686件"

まとめ

簡単なサンプルですがElixirのシンタックスの特徴をここまで書いてみました。 私個人の感想ですが次のように思いました。

  • パイプライン演算子、パターンマッチがアプリケーションを書く上で実用的
  • 並行処理能力の為にプログラマーに苦労を強いる言語は使いたくなかったが、Elixirは並行処理能力とRubyのような開発生産性がある(つまり、書いていて楽しい)

今回は分散プログラミングやエラー処理について全く書いていないので、このあたりは次回書く予定です。