Elixir Advent Calendar 2014 23日目です.
動的型付け言語である Erlang で,静的に型エラーをチェックするためのツール Dialyzer というものがあります.
日本語で Web 上に公開されている情報だと33. 型仕様とErlangが一番わかりやすいでしょう.
この文章では Dialyzer を Elixir で使う方法をいくつか紹介すると同時に,Dialyzer がどんなものか雰囲気がつかめることを願って書いています.
手動で設定する
Programming Elixir という Elixir を始めるとっかかりにするにはとても良い本があります.
その中の Appendix 2 に Type Specifications and Type Checking という章があり,Elixir で Dialyzer を使う方法を紹介しています.試してみましょう.
プロジェクトを作る
まずはプロジェクトを作ります.今回は simple
という名前にしました.
$ mix new simple
(...snip...)
$ cd simple
型付情報を用意する
lib/simple.ex
を以下のように編集します.
dialyzer の挙動を理解するため,この時点では関数の中身はまだ実装しません.
defmodule Simple do
@type atom_list :: list(atom)
@spec count_atoms(atom_list) :: non_neg_integer
def count_atoms(list) do
# ...
end
end
Dialyzer を動かす
プロジェクトをコンパイルする
- Dialyzer は ErlangVM である BEAM に対して行う
- Erlang や Elixir をコンパイルすると,拡張子が
.beam
な BEAM ができる
ので,まずは Elixir をコンパイル ( mix compile
) します.すると _build
以下にファイルが作られます.
$ mix compile
lib/simple.ex:4: warning: variable list is unused
Compiled lib/simple.ex
Generated simple.app
$ tree _build/
_build/
└── dev
└── lib
└── simple
└── ebin
├── Elixir.Simple.beam
└── simple.app
4 directories, 2 files
Dialyzer が利用する基本的な情報を準備する
次に
- Dialyzer はこのプログラムの実行に利用する全てのライブラリの静的な仕様を必要とする
ため,これを生成して保存しておかなければなりません.
このファイルのことを PLT(Persistent Lookup Table) といいます.
今回は Erlang の基本的なライブラリ (ert) と Elixir の実行用ライブラリの仕様が必要です.
これを生成するためには,ライブラリのパスを知らなければならなりません.
ライブラリのパスは
$ iex -S mix
Erlang/OTP 17 [erts-6.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Interactive Elixir (1.0.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> :code.lib_dir(:elixir)
'/usr/local/Cellar/elixir/1.0.2/bin/../lib/elixir'
で得られた結果に /ebin
を加えたものになります.
つまり,今回ですと /usr/local/Cellar/elixir/1.0.2/bin/../lib/elixir/ebin
です.
それではこのパスを利用して PLT を作成しましょう.この処理には数分くらいかかります.
$ dialyzer --build_plt --apps erts /usr/local/Cellar/elixir/1.0.2/bin/../lib/elixir/ebin
Compiling some key modules to native code... done in 0m56.23s
Creating PLT /Users/niku/.dialyzer_plt ...
(...snip...)
done (passed successfully)
Dialyzer 実行
できたら dialyzer を動かしてみましょう.
$ dialyzer _build/dev/lib/simple/ebin
Checking whether the PLT /Users/niku/.dialyzer_plt is up-to-date... yes
Proceeding with analysis...
simple.ex:3: Invalid type specification for function 'Elixir.Simple':count_atoms/1. The success typing is (_) -> 'nil'
done in 0m0.48s
done (warnings were emitted)
書いてある仕様と合わないというメッセージが表示されました.
プログラムには @spec count_atoms(atom_list) :: non_neg_integer
と書いてあり,
non_neg_integer
( 負ではない整数 ) が返る仕様なのですが,実際には nil
が返っているためです.
仕様を満たすとどうなるでしょうか. lib/simple.ex
の内容を書き換えてみましょう.
defmodule Simple do
@type atom_list :: list(atom)
@spec count_atoms(atom_list) :: non_neg_integer
def count_atoms(list) do
length list # ここを書き換え
end
end
書き換えたら,コンパイルして beam ファイルを生成してから dialyzer にかけます.
$ mix compile
Compiled lib/simple.ex
Generated simple.app
$ dialyzer _build/dev/lib/simple/ebin
Checking whether the PLT /Users/niku/.dialyzer_plt is up-to-date... yes
Proceeding with analysis... done in 0m0.45s
done (passed successfully)
今度はうまくいきました.
さらにもう一つモジュールを追加して試してみましょう.
lib/simple/client.ex
というファイルを新たに追加します.
defmodule Client do
@spec other_function() :: non_neg_integer
def other_function do
Simple.count_atoms [1, 2, 3]
end
end
追加したら,コンパイルして dialyzer にかけてみましょう.
$ mix compile
Compiled lib/client.ex
Generated simple.app
$ dialyzer _build/dev/lib/simple/ebin
Checking whether the PLT /Users/niku/.dialyzer_plt is up-to-date... yes
Proceeding with analysis...
client.ex:3: Function other_function/0 has no local return
client.ex:4: The call 'Elixir.Simple':count_atoms([1 | 2 | 3,...]) breaks the contract (atom_list()) -> non_neg_integer()
done in 0m0.44s
done (warnings were emitted)
新しいエラーが出ていますね.
client.ex:3: Function other_function/0 has no local return
で other_function
が何も返さないと書いてあるようです.
これには次の行
client.ex:4: The call 'Elixir.Simple':count_atoms([1 | 2 | 3,...]) breaks the contract (atom_list()) -> non_neg_integer()
が関係しています.
dialyzer は 4 行目で count_atoms
の (atom_list()) -> non_neg_integer()
が守れていない 1 ため,ここでエラーになる(=例外を発生させる)だろうと推測します.
すると 3 行目の other_function
関数全体としては例外を発生させるので,返り値としては何も返さないであろうと推測しているわけです.
賢いですね.
それではこれを直してみましょう.再度 lib/client.ex
を修正します.
defmodule Client do
@spec other_function() :: non_neg_integer
def other_function do
Simple.count_atoms [:a, :b, :c]
end
end
$ mix compile
Compiled lib/client.ex
Generated simple.app
$ dialyzer _build/dev/lib/simple/ebin
Checking whether the PLT /Users/niku/.dialyzer_plt is up-to-date... yes
Proceeding with analysis... done in 0m0.45s
done (passed successfully)
うまくいったようです.
Dialyxir を使う
Mix tasks to simplify use of Dialyzer in Elixir projects.
と謳っている Dialyxir というライブラリがあるので,同じように使ってみましょう.
README を参考に作業します.その前に,上で手動で作った PLT ファイルも消しておきましょう.
$ cd ~
$ rm .dialyzer_plt
$ git clone git@github.com:jeremyjh/dialyxir.git
Cloning into 'dialyxir'...
remote: Counting objects: 146, done.
remote: Total 146 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (146/146), 26.78 KiB | 0 bytes/s, done.
Resolving deltas: 100% (58/58), done.
Checking connectivity... done.
$ cd dialyxir
$ mix archive.build
Compiled lib/dialyxir/helpers.ex
lib/mix/tasks/dialyzer.ex:40: warning: unused import Enum
Compiled lib/mix/tasks/dialyzer.ex
Compiled lib/mix/tasks/dialyzer.plt.ex
Generated dialyxir.app
Generated archive dialyxir-0.2.6.ez with MIX_ENV=dev
$ mix archive.install dialyxir-0.2.6.ez
Are you sure you want to install archive dialyxir-0.2.6.ez? [Yn]
* creating /Users/niku/.mix/archives/dialyxir-0.2.6.ez
$ mix dialyzer.plt
Starting PLT Core Build ... this will take awhile
(...snip...)
done in 3m50.05s
done (passed successfully)
mix dialyzer.plt
には数分程度時間がかかりました.
それでは再び simple プロジェクトに dialyzer をかけてみましょう.
$ cd simple
$ mix compile
$ mix dialyzer
Starting Dialyzer
dialyzer --no_check_plt --plt /Users/niku/.dialyxir_core_17_1.0.2.plt -Wunmatched_returns -Werror_handling -Wrace_conditions -Wunderspecs /Users/niku/projects/dialyzer-samples/simple/_build/dev/lib/simple/ebin
Proceeding with analysis... done in 0m1.72s
done (passed successfully)
先ほど面倒だった dialyzer のセットアップがなくて,かわりに mix dialyzer
のコマンド一つですんでいますね.
dialyzer を普段使うなら,こちらを利用した方が楽できそうです.
Dialyze を使う
Dialyxir はシステム全体に対して mix dialyzer
というコマンドを提供するツールでした.
それに対し Dialyze はプロジェクト毎に mix dialyze
というコマンドを提供するツールのようです.
README を参考に作業しましょう.simple プロジェクトの mix.exs
ファイルを以下のように書き換えます.
defmodule Simple.Mixfile do
use Mix.Project
def project do
[app: :simple,
version: "0.0.1",
elixir: "~> 1.0",
deps: deps]
end
# Configuration for the OTP application
#
# Type `mix help compile.app` for more information
def application do
[applications: [:logger]]
end
# Dependencies can be Hex packages:
#
# {:mydep, "~> 0.3.0"}
#
# Or git/path repositories:
#
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
#
# Type `mix help deps` for more examples and options
defp deps do
[{:dialyze, "~> 0.1.3"}] # この行を追加
end
end
そして sample プロジェクトで以下のように実行してみます.
$ cd sample
$ mix do deps.get, deps.compile
Running dependency resolution
Unlocked: dialyze
Dependency resolution completed successfully
dialyze: v0.1.3
* Getting dialyze (Hex package)
Checking package (https://s3.amazonaws.com/s3.hex.pm/tarballs/dialyze-0.1.3.tar)
Fetched package
Unpacked package tarball (/Users/niku/.hex/packages/dialyze-0.1.3.tar)
==> dialyze
Compiled lib/mix/tasks/dialyze.ex
Generated dialyze.app
$ mix dialyze
Compiled lib/simple.ex
Compiled lib/client.ex
Generated simple.app
Finding applications for analysis
Finding suitable PLTs
(...snip...)
Analysing 2 modules with dialyze_erlang-17.4_elixir-1.0.2_deps-dev.plt
これはすんごい時間かかりました.十数分くらい.もしかしたら PLT の生成対象としているモジュールの数に違いがあるかもしれません.2
成功だと何も出ないようなので,lib/client.ex
のメソッドの中身を Simple.count_atoms [1, 2, 3]
に戻して実行してみましょう.
$ mix dialyze
Finding applications for analysis
Finding suitable PLTsLooking up modules in dialyze_erlang-17.4_elixir-1.0.2_deps-dev.pltFinding applications for dialyze_erlang-17.4_elixir-1.0.2_deps-dev.plt
Finding modules for dialyze_erlang-17.4_elixir-1.0.2_deps-dev.plt
Checking 1081 modules in dialyze_erlang-17.4_elixir-1.0.2_deps-dev.pltFinding modules for analysis
Analysing 2 modules with dialyze_erlang-17.4_elixir-1.0.2_deps-dev.plt
lib/client.ex:3: Function other_function/0 has no local return
lib/client.ex:4: The call 'Elixir.Simple':count_atoms([1 | 2 | 3,...]) breaks the contract (atom_list()) -> non_neg_integer()
ふむふむ,先ほどと同じように失敗が表示されましたね.
まとめ
- Elixir でも Erlang の静的型エラーチェックツール Dialyzer が利用できることがわかりました.
- 手動だと少し面倒なので,
mix
コマンドから実行できるDialyxir
やDialyze
というツールがあります
個人的には
- 手動だと面倒なので,ツールを使いたい.
- チェックの結果は
Dialyxir
の方が速く表示される
ので,2014-12-23 現在 Elixir で dialyzer を利用するなら Dialyxir
がいいかなあと思いました.
明日は @keithseahus さんです.たのしみー!