Note
訳注: -compiler attributeは、プログラミングErlangに準拠して、「モジュール属性」と しています。termはそのままtermとしています。
原文: | http://www.erlang.org/eeps/eep-0008.html |
---|---|
EEP: | 8 |
Title: | 型と関数の仕様定義 |
Version: | $Revision: 52 $ |
Last-Modified: | $Date: 2008-10-30 15:20:25 +0100 (Thu, 30 Oct 2008) $ |
Author: | Tobias Lindahl [tobias(dot)lindahl(at)it(dot)uu(dot)se], Kostis |
Sagonas: | [kostis(at)it(dot)uu(dot)se] |
Status: | Draft |
Type: | Standards Track |
Content-Type: | text/x-rst |
Created: | 2-Dec-2007 |
Erlang-Version: | R12B |
Post-History: | None |
このEEPは、ある型や、効率的にすべてのErlangの語彙(term)のサブタイプを 宣言するためのErlang言語拡張について説明します。これらの型は、レコードの フィールドや、関数の引数、返り値で使用することができます。
型情報を利用することで、関数のインタフェースを説明したり、 Dialyzerなどのバグ検出ツールに対してより詳しい状況説明を提供したり、 Edocなどのさまざまな形式でプログラムのドキュメントを生成することができる、 ドキュメンテーションツールが詳しい情報を取得できるようになります。 このドキュメントで説明している型システムに期待されることは、Edocで使用されている、 @typeや@specなどのコメントベースの説明よりも使われるようになって、置き換わることです。
型はErlangのtermのセットに対して説明します。型は、既定の型の集合で構成され、 組み立てられます。既定の型というのは、integer(), atom(), pid()などになります。これについては、後ほど説明します。 既定の型は、これらの型に属す、Erlangのtermの集合によって表現されます。 たとえば、atom()はアトムの集合を意味します。
数値とアトムのシングルトン型としては、数値の-1, 42、あるいはアトム の’foo’, ‘bar’が該当します。
他のすべての型は、既定の型、もしくはシングルトン型の集合を使って組み立てていきます。 もし、あるタイプと、そのサブタイプの両方を構成要素とする型があったとすると、 サブタイプは吸収されてなくなり、そのサブタイプは、 集合の要素として最初から定義されていなかったかのように扱われます。 例えば以下のような集合があったとします:
atom() | 'bar' | integer() | 42
これは以下のtermのセットで定義された型集合と同じ意味になります:
atom() | integer()
型同士の間にあるサブタイプの関係により、 すべてのErlangのtermを含む最上位要素のany()から、 termの空集合を表す最下層の要素の``none()``との間で、 すべての型はラティス構造を形成します。(訳注:完全な木構造にはならない、という意味だと思われる)
以下に示されているが、既定の型の集合です。また、 型に関するシンタックスについても示します:
Type :: any() %% トップの型。すべてのErlangのtermを含む
| none() %% 最下層の方。termを含まない
| pid()
| port()
| ref()
| [] %% nil
| Atom
| Binary
| float()
| Fun
| Integer
| List
| Tuple
| Union
| UserDefined %% ユーザ定義型の項で説明する
Union :: Type1 | Type2
Atom :: atom()
| Erlang_Atom %% 'foo', 'bar', ...
Binary :: binary() %% <<_:_ * 8>>
| <<>>
| <<_:Erlang_Integer>> %% 基数(Base size)
| <<_:_*Erlang_Integer>> %% 単位サイズ(Unit size)
| <<_:Erlang_Integer, _:_*Erlang_Integer>>
Fun :: fun() %% あらゆる関数
| fun((...) -> Type) %% あらゆる引数と返り値のタイプ
| fun(() -> Type)
| fun((TList) -> Type)
Integer :: integer()
| Erlang_Integer %% ..., -1, 0, 1, ... 42 ...
| Erlang_Integer..Erlang_Integer %% 整数の範囲を示したもの
List :: list(Type) %% Proper list ([]-終端)
| improper_list(Type1, Type2) %% Type1=中身, Type2=終端
| maybe_improper_list(Type1, Type2) %% Type1とType2は↑と同様
Tuple :: tuple() %% あらゆるサイズのタプルに適合
| {}
| {TList}
TList :: Type
| Type, TList
リストは一般的によく使用されるため、リストには短縮型表記法がいくつか設定されています。 list(T) は短縮して、 [T] と書くことができます。 [T,...] という短縮形はT型の要素を持つ、空でないリストを表します。 [T]と[T,...]の唯一の違いは、 [T]は空のリストになる可能性があるが、 [T,...]は空になることはないという点だけです。
注意して欲しいのは、 list() の短縮形、 つまり未知の型を要素に持つリストの短縮形は [_](あるいは[any()])になります。[]ではありません。 []という表記は、空のリストに対するシングルトン型を表します。
表記を便利にするために、以下のような型も組み込みで定義されています。 表の中の型の集合に対する、定義済みの別名として、見ることができます。 いくつかの型の集合は、これからの説明の中で頻繁に使います。
組み込み型 | 同値の定義 |
---|---|
term() | any() |
bool() | ‘false’ | ‘true’ |
byte() | 0..255 |
char() | 0..16#10ffff |
non_neg_integer() | 0.. |
pos_integer() | 1.. |
neg_integer() | ..-1 |
number() | integer() | float() |
list() | [any()] |
maybe_improper_list() | maybe_improper_list(any(), any()) |
maybe_improper_list(T) | maybe_improper_list(T, any()) |
string() | [char()] |
nonempty_string() | [char(),...] |
iolist() | maybe_improper_list(nchar() | binary() | iolist(), binary() | []) |
module() | atom() |
mfa() | {atom(), atom(), byte()} |
node() | atom() |
timeout() | ‘infinity’ | non_neg_integer() |
no_return() | none() |
既定の型、および、組み込み型と同じ名前の型をユーザが定義することはできません。 これはコンパイラによってチェックされ、問題があれば、 コンパイルエラーとして結果が通知されます。 最初に説明した目的のために、もし組み込み型に対して同様のことを行った場合には、 警告が発生します。
NOTE: 以下のような組み込みのリスト型も存在しますが、 ほとんど使われないことを想定しています。そのために長い名前がついています。
nonempty_maybe_improper_list(Type) :: nonempty_maybe_improper_list(Type, any())
nonempty_maybe_improper_list() :: nonempty_maybe_improper_list(any())
以下の二つのは型は期待されるErlangのtermのセットを定義するのに使用されます(訳不安)。
nonempty_improper_list(Type1, Type2) nonempty_maybe_improper_list(Type1, Type2)
また、便利に使うために、レコード記法も使用できるようになっています。 レコードは対応するタプルを短く書く記法になります。
Record :: #Erlang_Atom{}
| #Erlang_Atom{Fields}
レコードは型情報も入れることができるように拡張されました。これについての詳 細はレコード宣言の中の型情報にて説明していきます。
今までに見てきたように、型の基本的なシンタックスは丸括弧が後ろについたアトムになります。 ‘type’コンパイラ属性を利用して新しい型を宣言する方法は以下のようになります。
-type my_type() :: Type.
型名は、後ろに丸括弧のついたアトム(この例では ‘my_type’)になります。 Typeは今までのセクションで説明してきたような型です。現在の制限としては、 Typeに指定できるのは、既定の型、もしくはそれまでに出てきたユーザ定義型 のみになります。この制限に引っかかると、コンパイルエラーとして通知されます。 同様の制限がレコードに関しても存在します。
この制約があるために、現在では、再帰的な型の定義を一般的に行うことはできません。 この制約をどのようにはずしていくか、ということに関しては今後の課題になります。
型宣言では、型変数を丸括弧の中に含めることによりパラメータ化することもできます。 型変数を使うための文法は、Erlangの変数と同じで、最初の文字を大文字から開始します。 当然、これらの変数は、宣言の右側に書くことができますし、書かなければなりません。 具体的なサンプルを以下に示します。
-type orddict(Key, Val) :: [{Key, Val}].
レコードに含まれる、フィールドの型についても、レコード宣言の中で定義するこ とができます。
-record(rec, {field1 :: Type1, field2, field3 :: Type3}).
型の指定がないフィールドは、デフォルトではany()の型であるという扱いになります。上記のサンプルは以下の書き方を省略したのと同じになります。
-record(rec, {field1 :: Type1, field2 :: any(), field3 :: Type3}).
フィールドの初期値がある場合には、以下のように初期化が終わった後に型宣言を 置く必要があります。
-record(rec, {field1 = [] :: Type1, field2, field3 = 42 :: Type3}).
当然のことながら、フィールドの初期値は、指定された型(複数選択肢がある場合はそのひとつに) に準拠した値である必要があります。これはコンパイラによってチェックされ、 問題があれば、コンパイルエラーになります。初期値が存在しないフィールドについては、 ‘undefined’というシングルトン型がすべての型宣言に対して追加されます。 実際の例で説明すると、次に説明する2つのレコードは、処理系の中では、 まったく同じものとして扱われます。
-record(rec, {f1 = 42 :: integer(),
f2 :: float(),
f3 :: 'a' | 'b').
-record(rec, {f1 = 42 :: integer(),
f2 :: 'undefined' | float(),
f3 :: 'undefined' | 'a' | 'b').
これらの理由から、可能な場合には常に、レコードの初期化を行うことをお勧めします。
あらゆるレコードは、型情報を持つか持たないか、のどちらかになります。一度定義すると、 下記のようなシンプルなシンタックスで型を使用することができます。
#rec{}
さらに、レコードのフィールドは使用時にも仕様を設定することができます。 レコード型を使用するときに、以下のようにフィールドに関する型情報 を足すことで、指定することができます。
#rec{some_field :: Type}
この場合、型情報が設定されなかったフィールドに関しては、オリジナルの レコード宣言の情報が設定されているものとして実行します。
関数の仕様(契約)
新しいモジュール属性の 'spec' を使用することで、関数の契約(あるいは仕様) を記述することができます。基本的な書き方のルールは以下のようになります。
-spec Module:Function(ArgType1, ..., ArgTypeN) -> ReturnType.
関数の引数の数は、実際の関数と、使用の宣言で一致している必要があります。 もし一致しない場合には、コンパイラエラーになります。
この形式の書き方はヘッダファイル(.hrl)の中で、エクスポートされた関数の型情報 を宣言するのにも使用することができます。これらの情報が書かれたヘッダファイル をインクルードすると、これらの関数がインポートされて(場合によっては暗黙的に インポートされる場合もあります)使用できるようになります。
モジュール内部だけで使用する場合には、ほとんどの場合は下記のような、モジュール名を 省略した短縮系で使用されます。
-spec Function(ArgType1, ..., ArgTypeN) -> ReturnType.
また、ドキュメント化する目的で、引数に名前を設定することもできます。
-spec Function(ArgName1 :: Type1, ..., ArgNameN :: TypeN) -> RT.
関数定義は、オーバーロードさせて、複数のタイプを持たせることができます。 その場合は、セミコロン(;)を使用して、それぞれのパターンを区切って指定します。
-spec foo(T1, T2) -> T3
; (T4, T5) -> T6.
現在の制限としては、引数の型がオーバーラップしていると、コンパイラが 警告を発します。エラーにはなりません。例えば、以下のような仕様定義を すると警告が発生します。
-spec foo(pos_integer()) -> pos_integer()
; (integer()) -> integer().
型の変数を使って、関数の入出力の引数関係の定義を行うことができます。 以下のサンプルでは、ポリモーフィックな識別を行う関数の定義を行っています。
-spec id(X) -> X.
しかし、上記の定義では入力と出力の型について制約を与えることはできません。 ガードのように、特定の型のサブタイプであるという制約をかけるやり方を使用することができます。
-spec id(X) -> X when is_subtype(X, tuple()).
数量の境界に関しても、制約をかけることができます。現在は、 is_subtype/2ガードは、’spec’属性中でのみ使用することができます。
is_subtype/2制約のスコープは、それが適用された後の (...) -> RetType定義までになります。混乱を避けるために、 以下のような引数のオーバーロードがある場合には、それぞれの制約の定義では、 それぞれ別の変数を使用することをおすすめします。
-spec foo({X, integer()}) -> X when is_subtype(X, atom())
; ([Y]) -> Y when is_subtype(Y, number()).
Erlangの関数の中には値を返さない物もあります。サーバを定義するものであったり、 以下のように例外を投げるのに使用される関数であったり、というのが主要なケースです。
my_error(Err) -> erlang:throw({error, Err}).
このような関数においては、no_return()という特殊な返り値を返すものとして、定義することをおすすめします。
-spec my_error(term()) -> no_return()
現在の仕様の制限
現在の使用の一番大きな制約は、再帰を用いた型の定義ができないことです。