Elixirで試しに何か書いてみる(その1)

[**\[翻訳\] Elixir - 次に来る大物Web言語**](http://qiita.com/HirofumiTamori/items/0dfdbada30c7d8f183fd)を翻訳していて**Elixir**に興味を持ったので試しに簡単なプログラム…とはいえ**Hello, Worldよりはマシなもの**を書いてみました。またも**Webスクレイピング**です。
まずは並行動作なし、逐次実行のところまで。つまりElixir/Erlangの最大のメリットのひとつは**使っていません**。
**その2**で並行処理を取り入れて改良予定です。
→続きを書きました    [**Elixirで試しに何か書いてみる(その2)**](http://qiita.com/HirofumiTamori/items/7936ed9c6f73f4c9b475)
そのまた続き
[**Elixirで試しに何か書いてみる(その3) - Elixirのアプリケーション**](http://qiita.com/HirofumiTamori/items/06ab8e85c25f118f8e72**)
[**Elixirで試しに何か書いてみる(その4) - Taskを使って簡単にする**](http://qiita.com/HirofumiTamori/items/ed131104494f4bd2f695)
[**Elixirで試しに何か書いてみる(その5) - 失敗したらやり直す**](http://qiita.com/HirofumiTamori/items/dde5d95765c68f2fef3e)
もご覧ください(しぶとく更新し続けています)。

[**2015/6/5 追記:GitHubのHTML仕様変更とriot.jsのURL変更に対応して修正しました**]
[**2015/8/29 追記:shibacowさんのコメントによりソースを修正しました**]

------

## 準備

### Elixirのインストール

まずこちらの[**Elixir**本家サイト](http://elixir-lang.org/install.html)にしたがって**Elixir**(と**Erlang**及び**mix**(ビルドツール))をインストールします。

1. **Mac**だと**brew**一発で入ります。
```$ brew install elixir```

2. **Windows**のコンパイル済みバイナリのインストーラでも特に問題なく完了します。
3. **Ubuntu**はインストールの指示にもありますが、標準のレポジトリのErlangが古いのでレポジトリを追加する必要があります。その後は
```sudo apt-get update```
```sudo apt-get install elixir``` でインストールが完了します。
例によって**Beagle Bone Black**(Ubuntu 14.04)でもやってみました。こちらはレポジトリにコンパイル済みのものはないのでソースからビルドします。時間は相当かかりますがotp(Erlang)もElixirも**問題なくビルド**できました。Beagle Bone Blackよりリッチな環境の**RaspberryPi2**でなら楽勝でしょう[^1]。
[^1]: Erlangの分散実行環境って実はIoTに向いているのでは？と考えています。


### vimプラグインを入れよう

**NeoBundle**を使っている人は

    NeoBundle "elixir-lang/vim-elixir"

を追加するとかなりいい感じになります。**Elixir**は**Haskell**や**Python**のようなオフサイドルール(インデント・ブロック)ではありませんが、関数呼び出し時に()を省略できたり**Ruby**ぽく**do...end**でブロックが構成できるので簡易的でもいいのでインデントを調整してくれるこういうプラグインは助かります。

## お題

[**GoqueryでWeb スクレイピングしてGithubからリリース情報を取得する**](http://qiita.com/HirofumiTamori/items/e5068bfdbec16995b84e)をElixirに焼き直します。

実は以前Erlangに興味を持ったときにやはりWeb スクレイピングなプログラムを書きかけて…その時点では「**Erlangには文字列という概念はなくすべてバイト列です(キリッ)**」[^2]という状況だったので挫折していたのでそのリベンジも兼ねて。
[^2]: 前にも書きましたが今はかなりいろいろ改善されています。

## ライブラリについて

調べてみるとElixirには既に[**かなりの数の素敵ライブラリ群**](https://github.com/h4cc/awesome-elixir)が揃っています[^3]。
[^3]: 言語の設計目的上、やはりサーバー向けのものが多いようです。

今回利用したものは
1. [**HTTPoison**](https://github.com/edgurgel/httpoison)  HTTP Client ライブラリ。プロキシが使える。HTTPotionではプロキシが使えなかった。
2. [**Floki**](https://github.com/philss/floki)  HTMLパーザー。
3. [**Timex**](https://github.com/bitwalker/timex)  時刻・時間関連。
です。

## 標準ビルドツール**mix**

Elixirは最近の他の言語でもよくあるように標準ビルドツール**mix**[^4]が提供されています。
ライブラリの管理などもmixから**hex**(パッケージ管理ツール)を叩くことで実行されるのでちょっとしたプログラムを書こうとしたらmixの利用は必須です。

[^4]: 最近各言語いろいろなビルドツールが標準でついてきますがプログラム本体と同じ文法で設定が書ける、という点で**Clojure**の**Leiningen**を思い出しました。

### ソースツリーを作る

ソースツリーを置きたいディレクトリで
```$ mix new vercheckex```
のようにnewコマンドを実行してください。

```text
tamori@bbb1:~$ mix new vercheckex
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/vercheck.ex
* creating test
* creating test/test_helper.exs
* creating test/vercheck_test.exs

Your mix project was created successfully.
You can use mix to compile it, test it, and more:

    cd vercheckex
    mix test

Run `mix help` for more commands.
```

指示通り```mix test```を実行して問題なくテストが終了すればOKです。

### 依存ライブラリの指定 (mix.exs)

今回利用するライブラリ(HTTPoison, Floki, Timex)をmix.exsに追加します。
**github**のライブラリのREADME.mdに

```ex
def deps do
  [{:httpoison, "~> 0.6"}]
end
```
のような指定のしかたが書いてあるのでその通りmix.exs[^5]のdepsのところに追加します。
[^5]: **Leiningen**の**project.clj**みたいなもんです。

必要なものを全部設定したらまず依存性の解決です。

```bash
$ mix deps.get
```

これで必要なライブラリがgithubからダウンロードされ、ライブラリが展開されます。ただし、初回はパッケージ管理ツール**hex**がインストールされていなので、

```bash
Could not find hex, which is needed to build dependency :httpoison
Shall I Install hex? [Y/n]
```

と聞いてきます。Yを選択すると自動的にhexがインストールされて次に進みます(親切設計)。

### 本体ソース (vercheckex.ex)

全ソースで約70行です。短く書けました。
機能仕様については[**GoqueryでWeb スクレイピングしてGithubからリリース情報を取得する**](http://qiita.com/HirofumiTamori/items/e5068bfdbec16995b84e)を見てください。

```ex
defmodule VercheckEx do
  # requireで使用するライブラリを指定
  require HTTPoison
  require Floki
  require Timex
  use Timex
  def fetch_content(url, type) do
    ret = HTTPoison.get!( url ) # urlで指定されるページのデータを取得
    %HTTPoison.Response{status_code: 200, body: body} = ret
    # HTML bodyを取得する
    # HTMLパーザー Flokiで処理
    # 名前、リリース日時を取得
    {_,_,n} = Floki.find(body, "[itemprop=title]") |> List.first
    {_, date} = Floki.find(body, "time") |> Floki.attribute("datetime")
                                      |> List.first
                                      |> Timex.DateFormat.parse("{ISOz}")

    if(type == :type1) do # バージョン番号を取得
      {_,_,x} = Floki.find(body, ".tag-name span") |> List.first
    else
      {_,_,x} = Floki.find(body, ".css-truncate-target span") |> List.first
    end
    #UTC時刻をJSTに変更 
    date |> Timex.Date.Convert.to_erlang_datetime
         |> Timex.Date.from "Asia/Tokyo"
    {hd(n),hd(x),date} # 戻り値はタプル
  end

  def put_a_formatted_line(val) do # 1行出力
    {title, ver, date} = val
    l = title
    if String.length(title) < 8 do
      l = l <> "\t"
    end
    l = l <> "\t" <> ver
    if String.length(ver) < 8 do
      l = l <> "\t"
    end
    l = l <> "\t" <> Timex.DateFormat.format!(date, "%Y.%m.%d", :strftime)
    now = Timex.Date.now("JST")
    diff =  Timex.Date.diff( date, now, :days) # リリースから今日までの日数
    if diff < 14 do # 14日以内なら警告する。以前の仕事が2週間スプリントだった名残り。
      l = l <> "\t<<<<< updated at " <> Integer.to_string(diff) <> " day(s) ago."
    end
    IO.puts(l)
  end
end
urls = [
  {"https://github.com/jquery/jquery/releases", :type1},
  {"https://github.com/angular/angular/releases", :type1},
  {"https://github.com/facebook/react/releases", :type2},
  {"https://github.com/PuerkitoBio/goquery/releases", :type1},
  {"https://github.com/revel/revel/releases", :type2},
  {"https://github.com/lhorie/mithril.js/releases", :type1},
  {"https://github.com/riot/riot/releases", :type1},
  {"https://github.com/atom/atom/releases", :type2},
  {"https://github.com/Microsoft/TypeScript/releases", :type2},
  {"https://github.com/docker/docker/releases", :type1},
  {"https://github.com/JuliaLang/julia/releases", :type2},
  {"https://github.com/nim-lang/Nim/releases", :type1},
  {"https://github.com/elixir-lang/elixir/releases", :type2},
  {"https://github.com/philss/floki/releases", :type1},
  {"https://github.com/takscape/elixir-array/releases", :type2},
]

# 逐次呼出し→結果出力                                                                                                                                         HTTPoison.start
Enum.each(urls, fn(i) ->
  {u,t} = i
  res  = VercheckEx.fetch_content(u,t)
  VercheckEx.put_a_formatted_line res
end)

```

Rubyなどで「クラスを１個定義してそこに全機能入れて、スクリプトのメインから呼び出す」ぐらいの感じで書いてみました。
あんまり解説を読まずに書いちゃったのでElixirの流儀に適ってないかもしれません。**もっとこうしたほうがいい**というのがあれば教えてください。

#### Elixirらしいと思ったところ

1. コメント行が Erlangの %% から Rubyやシェルスクリプト流の # になっている
2. 値の取り出し方がタプル(tuple)による**パターンマッチ**(HTTPoisonの結果からbodyを取得するところなど)で行われる。
3. 文字列の扱いが楽。文字列結合は**<>**演算子。
4. Erlangと同じく**while**などによるループがない。今回の場合は**Enum.each**(collection, fn)による逐次実行。fnが無名変数でcollectionの要素が引数として渡される。
5. パイプ演算子(**>|**)が使いやすい。計算結果を次に来る関数の第1引数として渡しているのだがメソッドチェーンぽく直感的に読める(処理Aの結果を処理Bに渡してさらに処理Cに渡す…)

### ビルドと実行

```$ mix run vercheckex``` でビルドと実行が開始されます。もし初めて**Elixir/Erlang**を入れた場合はErlangのビルドツール**rebar**も入っていないでしょうから以下のようにメッセージが出ます。

```text
Could not find rebar, which is needed to build dependency :idna
I can install a local copy which is just used by mix
Shall I install rebar? [Yn] 
```
Yを選択して進めてください。
しばらくライブラリのビルドなどで時間がかかりますが…(初回のみ)…
うまく動いたら以下のように結果が出力されます。

```text
jquery          1.11.3          2015.04.29
angular         2.0.0-alpha.24  2015.05.20      <<<<< updated at 1 day(s) ago.
react           v0.13.2         2015.05.09      <<<<< updated at 12 day(s) ago.
goquery         v0.3.2          2013.09.08
revel           v0.12.0         2015.03.26
mithril.js      v0.1.34         2015.05.01
riotjs          v2.1.0          2015.05.20      <<<<< updated at 1 day(s) ago.
atom            v0.200.0        2015.05.20      <<<<< updated at 1 day(s) ago.
TypeScript      v1.5.0-beta     2015.04.28
docker          v1.6.2          2015.05.14      <<<<< updated at 7 day(s) ago.
julia           v0.3.8          2015.05.01
Nim             v0.11.2         2015.05.05
elixir          v1.0.4          2015.04.08
floki           v0.2.0          2015.05.04
elixir-array    1.0.1           2014.09.23
```

**でも順番にページの情報を取りに行ってるのでめちゃくちゃ遅いです！！**
次回、[**その2**](http://qiita.com/HirofumiTamori/items/7936ed9c6f73f4c9b475)では**プロセス**による並行動作を取り入れて高速化してみます。