6. 型(あるいはそれを欠いています)

6.1. 強烈な強さの型付け

これまでの「 (本当に)始めましょう! 」での例や、「 モジュール 」と「 関数の構文 」でモジュールや関数での型付けの例に気づいたかもしれませんが、変数の型を書いたり、関数の型を書いたりする必要はありませんでした。 パターンマッチをするときは、書いたコードがどんなものが適用するか分かっていませんでした。 {X,Y} というタプルに対して {atom, 123} でもいいし {"A string", <<"binary stuff!">>} でも {2.0, ["string","and",atoms]} でもなんでも適用します。

うまく動かない場合は、実行時に目の前にエラーが投げらます。これはErlangが動的型付けだからです。 すべてのエラーはランタイムに取得され、コンパイラは常にモジュールをコンパイルするときに、 「(本当に)始めましょう! 」での "llama + 5" の例のように実行時に失敗するようなところで大声で注意してくれません。

../_images/ham1.png

静的型付けと動的型付けでの典型的な争点は書かれているソフトウェアの安全性に関わる部分です。 よく、良いコンパイラによる静的型付けシステムは、強い熱意でたいていの起こり得るエラーをコード実行前に捉える、と言われています。 このような理由から静的型付け言語は動的型付け言語よりも安全だと言われています。 動的型付け言語と比較したときにはこれは事実ですが、Erlangはそうではなく、その欠点を改善する方法があります。 最も良い例はnine nineで稼働すると言われている Ericsson AXD 301 ATM switches です。これは100万行以上のErlangコードで書かれています。 これはErlangベースのシステムでは1つのコンポーネントも失敗していないということを言ってるわけでなく、一般的なスイッチシステムでは稼働時間の99.9999999%は落ちないということを言っていることをくれぐれも注意してください。 これはErlangはコンポーネントのうち1つが落ちたとしても、システム全体に影響を与えないようにすべきという考えがあるからです。 プログラマやハードウェア障害、あるいは なんらかの ネットワーク障害も考慮してあります。 言語自体にプログラムを異なるノードに配布して、予測不能なエラーを処理して、動作を止めたいという機能を含んでいるのです。

短く言えば、たいていの言語や型システムはプログラムからエラーを取り除くことを狙いとしていますが、Erlangはエラーはどうせ起きるんだから、エラーから確実に回復するようにしよう、という戦略に立っているということです。 Erlangの動的型付けシステムはプログラムと信頼性と安全性に対するバリアではありません。 これはなにか含みを持った言い方ですが、あとの章でどのように動作するか見てみましょう。

Note

動的型付けは歴史的に単純な理由から選択されました。はじめは、Erlangの実装者は動的型付け言語出身者が多く、彼らに取ってはErlangに動的型付けがあるのは自然なことだったのです。

Erlangは強い型付けでもあります。弱い型付け言語は暗黙的な型変換を行います。 もしErlangが弱い型付けだったら 6 = 5 + "1" というような操作もできたでしょう。 実際は、引数がおかしい旨の例外が投げられます:

1> 6 + "1".
** exception error: bad argument in an arithmetic expression
     in operator  +/2
        called as 6 + "1"

もちろん、あるデータ型を別のデータ型に変換したいというときはあるでしょう。通常の文字列をビット文字列に変換したいとか整数を浮動小数に変換したいとかです。 Erlangの標準ライブラリにはこういったことをする関数を多く提供しています。

6.2. 型変換

Erlangは多くの言語と同じように、項の型を別の型に変換します。Erlang自身では実装されていませんが、組み込み関数を使えばこういったことが可能です。 これらの関数はそれぞれ <type>_to_<type> という形式をとり、 erlang モジュール内に実装されています。 ここにいくつか例を示します:

1> erlang:list_to_integer("54").
54
2> erlang:integer_to_list(54).
"54"
3> erlang:list_to_integer("54.32").
** exception error: bad argument
in function  list_to_integer/1
called as list_to_integer("54.32")
4> erlang:list_to_float("54.32").
54.32
5> erlang:atom_to_list(true).
"true"
6> erlang:list_to_bitstring("hi there").
<<"hi there">>
7> erlang:bitstring_to_list(<<"hi there">>).
"hi there"

などです。ここでこの言語の欠点が見えてしまいました。<type>_to_<type>という組み合わせと使っていまうと、言語に新しい型が追加されるたびに、BIFに多くの変換用関数が追加されなければいけないからです! ここに、すべての変換用関数の一覧を載せます:

atom_to_binary/2, atom_to_list/1, binary_to_atom/2, binary_to_existing_atom/2, binary_to_list/1, bitstring_to_list/1, binary_to_term/1, float_to_list/1, fun_to_list/1, integer_to_list/1, integer_to_list/2, iolist_to_binary/1, iolist_to_atom/1, list_to_atom/1, list_to_binary/1, list_to_bitstring/1, list_to_existing_atom/1, list_to_float/1, list_to_integer/2, list_to_pid/1, list_to_tuple/1, pid_to_list/1, port_to_list/1, ref_to_list/1, term_to_binary/1, term_to_binary/2 and tuple_to_list/1.

たくさんの変換用関数がありますね。これらの関数のすべてが必要になることはないでしょうが、これらの型はこの本を通じてほとんど見ることになるでしょう。

6.3. データ型を守るために

Erlangの基本データ型は見た目で見分けることが簡単にできます。タプルは波括弧で、リストはカギ括弧、文字列はダブルクォーテーションで囲まれています。 パターンマッチのときにあるデータ型でなければいけないということがあります。例えば head/1 関数はリストだけを受け付け、それ以外の型だとパターンマッチ [H|_] で失敗します。

../_images/my-name-is1.png

しかし、範囲を指定できないので数値の時には問題があります。結果として、気温や運転の年齢制限の例などで関数内でガードを使いました。 ここで新たな検問所に引っかかりました。どのように数字やアトム、ビット文字列などのある特定の型にパターンマッチさせるようにガードを書けばいいのでしょうか?

この役割を果たす関数があります。それらは引数を1つだけ取り、もし型が正しければtrueを返し、違えばfalseを返します。 これらの関数はガード式の中で許された数少ない関数で、 型テストBIF と呼ばれています:

is_atom/1           is_binary/1
is_bitstring/1      is_boolean/1        is_builtin/3
is_float/1          is_function/1       is_function/2
is_integer/1        is_list/1           is_number/1
is_pid/1            is_port/1           is_record/2
is_record/3         is_reference/1      is_tuple/1

ほかのガード式と同じようにガード式を使えるところで使うことができます。 評価される項の型だけを返す関数がないのかな、とふと思うかもしれません。( type_of(X) -> Type という感じ) 答えは至極簡単です。Erlangは正しい事象に対してのプログラミングです。 あなたが起こると期待することに対してのみプログラミングすればよいです。 それ以外のものは直ちにエラーの原因となります。 これは気が狂ってるように思いえますが、第8章(エラー管理)での説明がそれをすっきりすること願います。 それまでは私を信用していてください。

Note

型テストBIFはガード式で使用を許されている関数の半数以上を占めています。 残りももちろんBIFですが、型テストとは関係ありません。一覧はこちらです: abs(Number), bit_size(Bitstring), byte_size(Bitstring), element(N, Tuple), float(Term), hd(List), length(List), node(), node(Pid|Ref|Port), round(Number), self(), size(Tuple|Bitstring), tl(List), trunc(Number), tuple_size(Tuple).

node/1self/0 は分散Erlangとプロセス/アクターと関係があります。 あとでこれらを使いますが、それらに触れる前に他のトピックに触れましょう。

Erlangのデータ構造は比較的限定的ですが、リストやタプルは他の複雑なデータ酵素を作るのに十分で何も心配する必要はないと思います。 二分木のノードは {node, Value, Left, Right} という形で表現できます。LeftとRightは同様のノードか空のタプルです。自己紹介も次のようにできます:

{person, {name, <<"Fred T-H">>},
{qualities, ["handsome", "smart", "honest", "objective"]},
{faults, ["liar"]},
{skills, ["programming", "bass guitar", "underwater breakdancing"]}}.

データが入った入れ子のタプルとリストによって、複雑なデータ構造やそれを扱う関数を作ることが出来ます。

Update

R13B04のリリースから、 binary_to_term/2 というBIFが加わりました。これによって、データを binary_to_term/1 と同じようにアンシリアライズできます。2番目の引数はオプションのリストです。 例えば [safe] を渡すとバイナリはよくわからないメモリを使い果たすアトムや 無名関数 はデコードされません。

6.4. 型ジャンキーのために

../_images/type-dance1.png

この節はいくつかの理由で静的型システムなしでは生きられないプログラマが読むために書かれたものです。 この節ではちょっと深い理論をふくんでいて、全員には理解されないものでしょう。 これから静的型解析をするツールやカスタム型を作る方法、さらにそれで安全にする方法などを簡単に紹介します。 これらのツールについてはあとでみなに分かりやすく紹介しますが、信頼性の高いErlangプログラムを核にはそれらを必ず使う必要はありません。 あとで書くので、それらのツールのインストールや実行などはあまり書きません。 再度、この節は進んだ型システムなしでは生きられない人向けに書きました。

何年もの間、Erlang上に型システムを作ろうという試みがありました。その試みの一つは1997年に、GHCのメイン開発者であるSimon Marlowと、Haskellの設計に関わりモナドの後ろにある理論に貢献したPhilip Wadlerによってなされました。 (型付けに関して 論文を読んでください ) Joe Armstrongが後に その論文にコメントしています

ある日、Philは私に電話してきて、次のようなことを言いました。 a) Erlangには型システムが必要だ b) 彼は型システムの小さいプロトタイプを書いた c) 彼は1年間の有給休暇があるからErlangの型システムを書こうと思っている。 そしてこう聞いてきました「面白くない?!」そして答えました「そうだね」

Phil WadlerとSimon Marlowは1年以上も型システムに取組みました。そしてその結果が[20]に発表されています。 このプロジェクトの結果はいくらか残念なものでした。最初だからと、型チェックが可能で、プロセス型と内部プロセスメッセージの型チェックをなくそうとしていました。

プロセスとメッセージはともにErlangのコアな機能なので、その機能がなぜ言語に取り入れられなかったか分かると思います。 他のErlangの型付けも失敗に終わりました。HiPEプロジェクト(Erlangのパフォーマンスをずっと良くするための試み)の努力によって、未だに使われている静的解析ツールのDialyzerを作りました。 Dialyzerには独自の型インタフェース機構が備わっています。

そこから出てきた型システムはSuccess Typingといって、Hindler-Milner型システムやSoft-typing型システムのコンセプトとは異なります。 Success Typeは概念は単純です:型インタフェースは四季ごとにきちんとした型を見つけようとはしませんが、推測した型は正しいと保証し、見つけた型エラーは実際にもエラーです。

この最も良い例は関数 and の実装から得られます。この関数は2つの真偽値をとって、両方共に’true’だったら’true’を返し、そうでなければ’false’を返します。 Haskellの型システムではこれは and :: bool -> bool -> bool と書かれます。もし and 関数がErlangで実装されるなら、次のように書けるでしょう:

and(false, _) -> false;
and(_, false) -> false;
and(true,true) -> true.

Success Typingの下では、この関数で推測される型は and(_,_) -> book() となります。 ここで _ は任意の値を意味します。こうなる理由は単純です。Erlangプログラムを実行して、 false42 という引数とともにこの関数を呼ぶと、結果は’false’になります。 パターンマッチで _ というワイルドカードを使うことは実践では成功を収めましたが、どんな関数でも引数でも片方が’false’である限り関数が動作してしまいます。ML型はもしこのような形で関数呼び出しがされた、適合を投げ捨ててしまうでしょう(そしてそのユーザは心臓発作を起こしてしまいます)Erlangではありません。 Success typesの実装に関する論文 を読む気になればもうちょっと分かると思います。そこには実際の動作よりもその原理が書いてあります。 私は型ジャンキーの方々にぜひその論文を読んでもらいたいと思います。とても興味深くて実践的な実装定義が書いてあります。

型定義と関数アノテーションの詳細についてはErlang Enhancement Proposal 8(EEP 8)に書いてあります。 もしErlangのSuccess Typingに興味があったら、 TypEr application も読んだり、Dialyzerを確認してみてください。両方共標準配布の一部です。 これらを使うには、 $ typer --help$ dialyzer --help と入力してみてください。 (Windowsでは typer.exe --helpdialyzer.exe --help となります)

TypErは関数の型アノテーションを生成するために使われています。この小さい FIFOの実装 を使ってみると、次のような型アノテーションを吐き出します:

%% File: fifo.erl
%% --------------
-spec new() -> {'fifo',[],[]}.
-spec push({'fifo',_,_},_) -> {'fifo',nonempty_maybe_improper_list(),_}.
-spec pop({'fifo',_,maybe_improper_list()}) -> {_,{'fifo',_,_}}.
-spec empty({'fifo',_,_}) -> bool().
../_images/fifo1.png

これはかなりあっています。 lists:reverse/1 がサポートしないので、不適切なリストは避けるべきです。 しかし誰かがこのモジュールのインタフェースを仲介にして、不適切なリストを渡したとします。 この場合、 push/2pop/2 は呼び出しでは問題ないと思いますが、そのうち例外を投げると思います。 これは手でガードを追加するか、型宣言を直すべきという事を行っています。 シグネチャ -spec push({fifo,list(),list()}_) -> {fifo,nonempty_list(),list()}. を追加して、 push/2 に不適切なリストを渡すことを想像してみましょう。 これをDialyzerでスキャンすると(型をチェックして適応させます)、エラーメッセージ “The call fifo:push({fifo,[1|2],[]},3) breaks the contract ‘<Type definition here>’”が出力されます。

Dialyzerはコードが他のコードを破壊してしまうようなときに警告を出します。警告を出す場合は、通常それはコードを破壊します。(もっと多くの情報を出します。例えば括弧が対応しないとか、一般的な矛盾についての警告をします) 多様なデータ型に関してもDialyzerで解析することができます。 hd() 関数はErlangプログラマはこういった型構文を使いませんが -spec([A]) -> A. という形でアノテーションされ、正しく解析されます。

Don’t drink too much Kool-Aid:

DialyzerとTypErでは推測できないものの中にコンストラクタ付き型クラスと一階型と再帰型があります。 Erlangでの型はただのアノテーションであって、コンパイラに明示的に指示しなければ実際のコンパイルで効果や制限を何も与えません。 型チェッカはすぐに動作する(それとも2年動いている)プログラムが型のバグを持っているかどうかは決して伝えてくれません。(バギーコードを正しく動かすことも出来ますが...)

再帰型は時々とてもおもしろいものですが、今の形でのTypErやDialyzerには出てきてほしくないものです。 (その理由は上に上げた論文を参照してください)再帰型を再現するために独自の型として1階層あるいは2階層の追加して試してみるのがいいでしょう。

それは完璧な型システムではなく、Scala, Haskell, OCamlが持っているような型システムほど厳格でもなく、強力でもありません。 また警告やエラーメッセージは通常ちょっと曖昧であまりユーザにやさしくないかもしれません。 しかし、動的な世界で生きられない、あるいはもうちょっと安全性がほしいのならば、適切な妥協でしょう。 TypErやDialyzerを倉庫にあるツールくらいに考えて、それ以上を期待しなければいいのです。

Update

R13B04から、再帰型はDialyzerの実験的機能として追加されました。 これは直前の Don’t drink too much Kool-aid でいっていることと違いますね。すいません。

また、 型の記述は公式になりました。 (変更の可能性がありますが)そして、EEP8に記載されているよりもより完全なものとなっています。