この記事は Erlang Advent Calendar 2015 の5日目です。よろしくお願いします。
わたしは普段の業務では PHP で Web アプリを作ったり、Objective-C, Java, C++ あたりでスマートフォンアプリを作っていて Erlang は休日に趣味でやるレベルの初級者です。
今回のアドベントカレンダーの題材として、いままで使ったことがない機能のひとつである、Dialyzer に入門してみました。この記事では Erlang/OTP アプリケーションの雛形の作成から、Dialyzer を実行するところまでの手順をまとめてみたいと思います。
そもそも Dialyzer とは?
動的型付け言語である Erlang をあたかも静的型がついているかのように扱えるようにするツールです。変数の型を定義したり、関数の仕様を記述したりするアノテーションをコード中に書いておくと、それを利用してコード全体を静的解析します。コンパイラだけでは拾えない、型や仕様に違反しているコードを見つけ出すことで、実行時のエラーを減らすことができます。
OTP アプリケーションの雛形
テンプレート作成からビルド、リリースまで統合的にやってくれるツールとして、Rebar と Erlang.mk を知っていますが、今回は Erlang.mk を使ってみます。最近 cowboy をいじっていて、同じ開発元が作っているということで都合がよかったので。
Git リポジトリの README.md や
Erlang.mk のチュートリアル http://erlang.mk/guide/ch02.html を参考に進めていきます。
Erlang.mk の取得
プロジェクトのディレクトリを作成して、wget で取ってきます:
$ mkdir hello_dialyzer
$ cd hello_dialyzer
$ wget https://raw.githubusercontent.com/ninenines/erlang.mk/master/erlang.mk
雛形の作成
Erlang.mk はなんと、ふつうの make
コマンドから起動するようになってます。make の自由度高すぎですね。雛形の作成を行うターゲットは bootstrap
です:
$ make -f erlang.mk bootstrap
雛形ができたら一度コンパイルしてみます:
$ make
すると以下のような OTP アプリケーションの標準的なフォルダ構成ができあがります:
.
├── Makefile
├── ebin
│ ├── hello_dialyzer.app
│ ├── hello_dialyzer_app.beam
│ └── hello_dialyzer_sup.beam
├── erlang.mk
├── hello_dialyzer.d
└── src
├── hello_dialyzer_app.erl
└── hello_dialyzer_sup.erl
Dialyzer の下準備
Erlang.mk のターゲットとして Dialyzer 関連のものが用意されているのでそれを使います。
まずはじめに一度だけ、PLT (Persistent Lookup Table, Dialyzer が既存のライブラリなどから型情報を解析結果を保存するところ)を生成する必要があります。これには、plt
というターゲットを指定します。
オプションについて
PLT の保存場所と、どのライブラリを解析するかをオプションで指定できるようです。場所についてはデフォルト値がプロジェクトの直下に .${プロジェクト名}.plt という名前でファイルが作られます。こちらは今回はこのままで行こうと思います。
ライブラリについてはデフォルトでは
- erts
- kernel
- stdlib
の3つが解析されます。これに少し追加してみます。追加するには、Makefile に PLT_APPS
という名前の変数を定義して追加したいモジュールを記述すればよいようです(保存場所の変更は DIAYZER_PLT
という変数で行います):
PROJECT = hello_dialyzer
PLT_APPS = mnesia crypto public_key reltool
include erlang.mk
さて
$ make plt
を実行します。結構時間がかかります。手元の Macbook pro 上の VirutalBox/Vagrant 環境で動く Centos7 (シングルコア割り当て)で4分弱かかりました。結果、.hello_dialyzer.plt という名前の 920kB のファイルが生成されました。これで Dialyzer 実行の準備ができました。
Dialyzer の実行
Dialyzer で自分のコードに実際に型チェックを掛けるターゲットはその名も dialyze
です:
$ make dialyze
Checking whether the PLT /vagrant/erlang/hello_dialyzer/.hello_dialyzer.plt is up-to-date... yes
Proceeding with analysis...
Unknown types:
wx:wx_colour/0
wx:wx_datetime/0
wx:wx_enum/0
wx:wx_object/0
wx:wx_wxHtmlLinkInfo/0
wxAuiManager:wxAuiManager/0
wxAuiNotebook:wxAuiNotebook/0
wxAuiPaneInfo:wxAuiPaneInfo/0
wxCursor:wxCursor/0
wxDC:wxDC/0
wxFont:wxFont/0
wxMenu:wxMenu/0
wxWindow:wxWindow/0
done in 0m0.69s
done (passed successfully)
GUI 関連?の項目で Unkonwn types と言われているのが少し気になりますが、passed successfully と出ているので成功かと思います。
失敗例
わざとエラーを出してみます。supervisor ビヘイビアの init/1 関数は戻り値として
Result = {ok,{SupFlags,[ChildSpec]}} | ignore
と記述されていますが、これを間違えて ok → ng と書いてしまったとします。すると、
$ make dialyze
Checking whether the PLT /vagrant/erlang/hello_dialyzer/.hello_dialyzer.plt is up-to-date... yes
Proceeding with analysis...
hello_dialyzer_sup.erl:10: The inferred return type of init/1 ({'ng',{{'one_for_one',1,5},[]}}) has nothing in common with 'ignore' | {'ok',{{'one_for_all',non_neg_integer(),pos_integer()} | {'one_for_one',non_neg_integer(),pos_integer()} | {'rest_for_one',non_neg_integer(),pos_integer()} | {'simple_one_for_one',non_neg_integer(),pos_integer()} | #{},[{_,{atom() | tuple(),atom(),'undefined' | [any()]},'permanent' | 'temporary' | 'transient','brutal_kill' | 'infinity' | non_neg_integer(),'supervisor' | 'worker','dynamic' | [atom() | tuple()]} | #{}]}}, which is the expected return type for the callback of supervisor behaviour
Unknown types:
wx:wx_colour/0
・・・中略・・・
wxWindow:wxWindow/0
done in 0m0.67s
done (warnings were emitted)
make: *** [dialyze] エラー 2
という具合に型違いを示すエラーが出ました。この場合でも、コンパイルは通るので、コンパイラではチェックできなかったエラーを Dialyzer によって検出できたということになります。
自分のコードにも型情報を書く
当然ながら、自分のコードにも型情報を書いておくことができます。記法は公式ドキュメントの Types and Function Specifications というところに載っています。(最初 Dialyzer の項目ばかり見ていてどこに書いてあるのかずっと探していました・・・)。
ほかには、すごいE本や、Programming Erlang (ジェット機本)にも詳しく解説されています。型の種類がたくさんあったりでやや分量が多めですがあまり難しくはない印象です。(まだ詳しく読めていません。)
ほんの少しだけ書いてみるとこんな具合のようです:
-spec hoge(integer()) -> integer().
hoge(A) -> ok.
1行目は hoge()
関数が整数を引数にとって整数を返す関数であることを宣言しています。しかしながら、実際の hoge()
関数の定義はそのようになっていないとします。(整数ではなく、アトム ok を返している)。この時に dialyzer を実行すると、エラーが出力されます:
$ make dialyze
Checking whether the PLT /vagrant/erlang/hello_dialyzer/.hello_dialyzer.plt is up-to-date... yes
Proceeding with analysis...
hello_dialyzer_sup.erl:16: Invalid type specification for function hello_dialyzer_sup:hoge/1. The success typing is (5) -> 'ok'
Unknown types:
・・・中略・・・
done in 0m0.69s
done (warnings were emitted)
make: *** [dialyze] エラー 2
まとめ
Erlang/OTP の静的型チェック機構である Dialyzer を試してみました。自分のコードにうまい型をつけられるようになるにはある程度練習が必要そうです・・・
しかし、既存コードにはエキスパートたちが書いた型づけがすでにあるわけなので、自分のコードに型を書くところまでいかなくても、既存コードを利用するところで Dialyzer は大きな助けになると思います。
コードを編集する度に手動で Dialyzer を掛けるのはやや煩雑な気がするので、ファイルの変更を監視して自動で実行してくれる仕組みを用意しておくとよさそうですね。