型(またはその欠如)
ダイナマイトのように強力な型付け
実際に始めるからの例、そしてモジュールや関数の構文からのモジュールや関数を打ち込む際に気づいたかもしれませんが、変数の型や関数の型を記述する必要はありませんでした。パターンマッチングを行う際、記述したコードは、何がマッチされるかを知る必要はありませんでした。タプル{X,Y}は、{atom, 123}と同様に、{"A string", <<"binary stuff!">>}、{2.0, ["strings","and",atoms]}、あるいは実際には何でもマッチさせることができました。
うまくいかない場合は、エラーがスローされますが、コードを実行したときだけです。これは、Erlangが*動的型付け*であるためです。すべてのエラーは実行時にキャッチされ、コンパイラは、実際に始めるの"llama + 5"の例のように、失敗する可能性のあるモジュールをコンパイルするときに、必ずしもエラーを報告するわけではありません。
静的型付けと動的型付けの支持者の間の古典的な摩擦点は、作成されるソフトウェアの安全性に関しています。頻繁に提案されるアイデアは、コンパイラによって熱心に適用される優れた静的型システムは、コードを実行する前に発生するのを待っているほとんどのエラーをキャッチするというものです。そのため、静的型付け言語は、動的型付け言語よりも安全であると見なされます。これは多くの動的言語と比較する場合に当てはまるかもしれませんが、Erlangは異議を唱え、確かにそれを証明する実績があります。最も良い例は、100万行以上のErlangコードで構成されるEricsson AXD 301 ATMスイッチで提供される、しばしば報告される*ナインナイン*(99.9999999%)の可用性です。これは、Erlangベースのシステムのコンポーネントに障害が発生しなかったことを示しているのではなく、一般的なスイッチシステムが計画された停止を含め、99.9999999%の時間利用可能であったことを示していることに注意してください。これは、Erlangが、1つのコンポーネントの障害がシステム全体に影響を与えるべきではないという考えに基づいて構築されているためです。プログラマーからのエラー、ハードウェア障害、または[一部の]ネットワーク障害が考慮されています。この言語には、プログラムを異なるノードに分散し、予期しないエラーを処理し、*決して*停止しないようにする機能が含まれています。
要するに、ほとんどの言語と型システムはプログラムをエラーフリーにすることを目指していますが、Erlangはエラーがとにかく発生すると仮定し、これらのケースを確実にカバーする戦略を使用しています。Erlangの動的型システムは、プログラムの信頼性と安全性の障壁ではありません。これは多くの予言的な話のように聞こえますが、後の章でどのように行われるかを見るでしょう。
**注:** 動的型付けは、歴史的に単純な理由で選択されました。最初に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標準ライブラリには、それを行うための多くの関数が用意されています。
型変換
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、tuple_to_list/1。
それは多くの変換関数です。この本では、これらの関数のすべてではないにしても、ほとんどすべてを見ることになるでしょうが、おそらくこれらの関数のすべてを必要とすることはないでしょう。
データ型をガードする
Erlangの基本的なデータ型は、視覚的に簡単に見つけることができます。タプルには中括弧、リストには角括弧、文字列は二重引用符で囲まれています。したがって、特定のデータ型を適用することは、パターンマッチングによって可能になりました。リストを取る関数head/1は、そうでなければマッチング([H|_])が失敗するため、リストのみを受け入れることができました。
ただし、範囲を指定できなかったため、数値に問題がありました。その結果、温度、運転年齢などに関する関数でガードを使用しました。 अब हम एक और रोडब्लॉक मार रहे हैं। हम एक ऐसा गार्ड कैसे लिख सकते हैं जो यह सुनिश्चित करता है कि पैटर्न एक विशिष्ट प्रकार के डेटा से मेल खाते हैं, जैसे संख्याएँ, परमाणु या बिटस्ट्रिंग?
このタスク専用の関数があります。それらは単一の引数を取り、型が正しい場合はtrueを、そうでない場合はfalseを返します。それらはガード式で許可されている数少ない関数の1つであり、*型テスト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は正しいケースのためにプログラミングすることです。何が起こるか、何を期待しているかだけをプログラミングします。それ以外のすべては、できるだけ早くエラーを引き起こすはずです。これは正気とは思えないかもしれませんが、エラーと例外で得られる説明は、うまくいけば物事をより明確にするでしょう。それまでは、私を信じてください。
**注:** 型テスト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/1とself/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"]}}.
これは、タプルとリストをネストしてデータを入力することにより、複雑なデータ構造を取得し、それらを操作する関数を構築できることを示しています。
更新
リリースR13B04では、BIF binary_to_term/2が追加されました。これにより、binary_to_term/1と同じ方法でデータを逆シリアル化できますが、2番目の引数はオプションリストです。[safe]を渡すと、バイナリに未知の原子または匿名関数が含まれている場合、メモリを使い果たす可能性があるため、デコードされません。
型中毒者のために
このセクションは、何らかの理由で静的型システムなしでは生きていけないプログラマーが読むことを目的としています。少し高度な理論が含まれており、すべての人が理解できるとは限りません。Erlangで静的型分析を行うために使用されるツール、カスタム型の定義、そしてその方法でより多くの安全性を確保する方法について簡単に説明します。これらのツールは、信頼性の高いErlangプログラムを作成するためにそれらを使用する必要がないことを考えると、本書の後半で誰でも理解できるように説明します。後でそれらを表示するため、インストール、実行などについてはほとんど説明しません。繰り返しますが、このセクションは、高度な型システムなしでは本当に生きていけない人のためのものです。
長年にわたり、Erlangの上に型システムを構築しようとする試みがいくつかありました。そのような試みの1つは1997年に、Glasgow Haskell Compilerのリード開発者の1人である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が生まれました。これは、独自の型推論メカニズムを備えた、現在も使用されている静的解析ツールです。
そこから生まれた型システムは、Hindley-Milner型システムやソフトタイピング型システムとは異なる概念である成功型に基づいています。成功型の概念はシンプルです。型推論はすべての式の正確な型を見つけようとせず、推論した型が正しく、検出した型エラーが本当にエラーであることを保証します。
最良の例は、関数`and`の実装です。これは通常、2つのブール値を受け取り、両方が真であれば 'true' を返し、そうでなければ 'false' を返します。Haskellの型システムでは、これは `and :: bool -> bool -> bool` と記述されます。`and` 関数をErlangで実装する必要がある場合、次の方法で行うことができます。
and(false, _) -> false; and(_, false) -> false; and(true,true) -> true.
成功型付けでは、関数の推論型は `and(_,_) -> bool()` になります。ここで、`_` は「何でも」を意味します。この理由は単純です。Erlangプログラムを実行し、この関数を引数 `false` と `42` で呼び出すと、結果は依然として 'false' になります。パターンマッチングで `_` ワイルドカードを使用することで、実際には、関数が機能するためには一方の引数が 'false' であれば、どのような引数でも渡すことができます。ML型では、この方法で関数を呼び出すとエラーが発生します(そしてユーザーは心臓発作を起こします)。Erlangではありません。成功型の実装に関する論文(成功型の実装)を読んで、この動作の背後にある理論的根拠を理解すると、より理解できるかもしれません。型にこだわる人にはぜひ読んでいただきたい、興味深く実用的な実装定義です。
型定義と関数アノテーションの詳細は、Erlang Enhancement Proposal 8(EEP 8)に記載されています。Erlangで成功型を使用することに興味がある場合は、標準ディストリビューションの一部であるTypErアプリケーションとDialyzerを確認してください。これらを使用するには、`$ typer --help` と `$ dialyzer --help` と入力します(Windowsの場合は、現在のディレクトリからアクセスできる場合、`typer.exe --help` と `dialyzer.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().
これはほぼ正しいです。`lists:reverse/1` は不適切なリストをサポートしていないため、避けるべきですが、モジュールのインターフェースをバイパスする人は、それをすり抜けて送信することができます。この場合、`push/2` と `pop/2` 関数は、例外が発生する前に数回呼び出しに成功する可能性があります。これは、ガードを追加するか、型定義を手動で絞り込む必要があることを示しています。シグネチャ `-spec push({fifo,list(),list()},_) -> {fifo,nonempty_list(),list()}.` と、不適切なリストを `push/2` に渡す関数をモジュールに追加するとします。Dialyzer(型をチェックして一致させる)でスキャンすると、「呼び出し fifo:push({fifo,[1|2],[]},3) は契約 '<型定義はこちら>' に違反しています」というエラーメッセージが出力されます。
Dialyzerは、コードが他のコードを壊す場合にのみ警告を発し、警告を発する場合、通常は正しいです(一致しない節や一般的な不一致など、他の多くのことについても警告を発します)。ポリモーフィックデータ型もDialyzerで記述および分析できます。`hd()` 関数は `-spec([A]) -> A.` でアノテーションを付け、正しく分析できますが、Erlangプログラマはこの型構文をあまり使用しないようです。
過度に期待しないでください
DialyzerとTypErに期待できないことのいくつかは、コンストラクタを持つ型クラス、一次型、および再帰型です。Erlangの型は、自分で強制しない限り、実際のコンパイルに影響を与えたり制限したりしない単なるアノテーションです。型チェッカーは、実行時に実際にエラーが発生しない場合(ただし、バグのあるコードが正しく実行されている可能性はあります)、現在実行できる(または2年間実行されている)プログラムに型バグがあると通知することはありません...
再帰型は非常に興味深いものですが、TypErとDialyzerの現在の形式で出現する可能性は低いでしょう(上記の論文でその理由が説明されています)。1つまたは2つのレベルを手動で追加することにより、再帰型をシミュレートする独自の型を定義することが、現時点でできる最善の方法です。
確かに、Scala、Haskell、Ocamlなどの言語が提案するものほど厳密で強力な、本格的な型システムではありません。また、警告メッセージとエラーメッセージは通常、少し不可解で、ユーザーフレンドリーではありません。ただし、本当に動的な世界に住むことができず、追加の安全性を望んでいる場合は、依然として非常に良い妥協案です。あなたの武器庫のツールとして期待してください。それ以上は期待しないでください。
更新
バージョンR13B04以降、再帰型はDialyzerの実験的な機能として利用できるようになりました。これにより、前の *過度に期待しないでください* は部分的に間違っています。私の不徳の致すところです。
型の公式ドキュメントも公開され(変更される可能性はありますが)、EEP8に記載されているものよりも完全なものになっています。