[追記 date="翌日"]帰る間際にあわててアップロードして、いくつか細かいミスがあったので修正しました。[/追記]
Erlangは静的な型チェックをまったくしないプログラミング言語ですが、データにはもちろん型(タイプ)があります。コンパイラはデータ型を気にしなくても、ドキュメンテーションではデータ型の記述が重要です。そこで、ドキュメンテーションのため(ひょっとすると、将来の型システム拡張のため?)に、型を記述する記法が使用されています。
Erlangの型記法は慣習的(conventional)なものなので、だいぶ“ゆらぎ”や“不整合”があるのですが、なるべく合理的な線を探りながら説明してみます。ネタ元は、アームストロングの本"Programming Erlang"の付録Aです。この型記法は、Erlangのmanページや一部の開発ツール*1で使用されています。
型の名前
型の名前は小文字で始まる名前で、最後に空な括弧「()」を付けます。例えば、integer(), file_name() とか。なんで、括弧を付けるかというと、そうしないとアトムと区別がつかなくなるから。アトムokだけからなる型(ユニット型、シングルトン型)は単にokと書きます。また、3つのアトムred, green, blueからなる列挙型は (red | green | blue) *2もうひとつ例を挙げると、bool() は (true | false) のこと。
うがった見方をすると、すべての型は型パラメータを持つ総称型だとみなして、関数呼び出しみたいな書き方をしているのかもしれません。list(char()) とかだと、そんな雰囲気もするでしょ。
基本的な型
基本データ型(primitive data types)を以下に列挙します。説明は要らないでしょう。
- atom(), binary(), float(), function(), integer(), pid(), port(), reference()
基本データ型の部分集合となっている型は:
tuple()は、任意のタプルを表すデータ型です。list()は型構成子で1つの引数を取ります。list(integer())は“整数のリスト”の型。
any()とnone()は特殊で、データ領域の全体集合(普遍集合)と空集合を表します。any()を使ってlist(any())とすると、“任意のリスト”の型です。any()の別名としてterm()も使います。none()は、無限に走り続けたり必ず例外を投げる関数の戻り値として使います。
構成された型
単純な型を組み合わせてより複雑な型を構成できます。既に述べたtuple()、list(any())は複合的な型ですが、一般に次のような型構成操作が使えます。
- タプルのパターン -- 例: {integer(), string()}は、第1項が整数で第2項が文字列であるタプルの型
- リスト -- 例: list(string())は、項目(要素)が文字列であるリストの型
- 選択(ユニオン、バリアント) -- 例: (integer() | float())は、整数または浮動小数点数を表す型。
- 列挙 -- 例: (yes | no) 選択と同じだが、いくつかのアトムからなる型。
- 関数のパターン -- 例: (fun(integer(), string()) -> bool())は、“第1引数が整数型、第2引数が文字列型であり、真偽値を返す関数”の型
新しい型の定義
「型式」と書いたら、誰でも「かたしき」じゃなくて「けいしき」と読むでしょうね。だから、type expression は型表現と呼ぶことにして:
- 新しい型の名前() = 型表現
の形で新しい型を定義します。例えば、
number() = integer() | float()
point() = {number(), number()}
polyline() = list(point())
型表現は、基本的な型や型の組み合わせを指定します。よく使う定義をこの書き方で示しておきます。
bool() = true | false
string() = list(char())
nil() = [] % 空リスト[]のみからなる型
iolist() = list(char() | binary() | iolist()) % 再帰的
deep_string() = list(char() | deep_string()) % 再帰的
レコード型
Erlangでは、ユーザー(プログラマ)が型システムを拡張する手段はありません。ただし、シンタックスシュガーとしてレコード定義が用意されています。
-record(person, {
name :: string(),
age :: integer(),
mail_address :: string(),
face :: binary()
}).
このレコード定義で定義された型は、実は {person, string(), integer(), string(), binary()}
のことであって、本質的に新しいデータ構造ではありません。つまり、上のレコード定義は、実質的に次の型定義と同じです*4。
person() = {person, string(), integer(), string(), binary()}
ここで、第1項のpersonは、レコードの目印となるアトムです。
関数の型仕様
([追記 date="2007-09-22"]このセクションに書いてあることには、僕の誤解が含まれます。「続・Erlangの型記法(間違い訂正)」に訂正が書いてあります。[/追記])
関数の名前、引数、戻り値をドキュメンテーションするとき、次のような形式を使います。
is_point(Data::any()) -> bool()
total_length(PL::polyline()) -> float()
winding_angle(PL::polyline(), BasePoint::point()) -> float()
「関数名(引数仕様) -> 戻り値型」という形ですね。引数仕様のところは「引数変数::型表現」を何個かカンマで区切って並べたものです。次のように書いてもかまいません。
winding_angle(PL, BasePoint) -> float()
PL :: polyline(),
BasePoint :: point()
慣例では、「::」の代わりに「=」が用いられます。あまり合理的ではないのですが、「PL = polyline()」と書いたら、「PLという変数は、以後ずっとpolyline()型だと約束する」という意味になります。manページなんかは、だいたいこの書き方。
winding_angle(PL, BasePoint) -> float()
PL = polyline(),
BasePoint = point()
引数変数を省略するときもあります。入れ子の関数呼び出しと間違えないでくださいね。
winding_angle(polyline(), point()) -> float()
正規表現が使えないのが不満
型表現list(T)は、[T]とも書きます。これは、「項目(要素)の型がTであるリストの型」です。しかし、「第1項目が整数で、それ以降は文字列が並ぶリストの型」をうまく表現する方法がありません。正規表現の繰り返しを表す記号「+」を使って [integer(), string()+] と書けるといいのですが、そうはできません。せいぜいできることは [integer() | string()] と書くことくらい。
リストの型を表すとき、「*」「+」「?」を自由に使ったパターン(正規表現)を書けると随分と自由度が増すんですがねぇ…(ブツブツ)。
([追記 date="2007-09-22"]型構成子consがあるので、正規表現と同値な表現はできそうです。「Erlangの型システムを定式化してみる」を参照してください。ただし、「*」「+」「?」を自由に使ったパターンが書けないという意味ではやはり正規表現は使えないと言えます。[/追記])
*1:EDoc(http://www.erlang.org/doc/apps/edoc/index.html)、Dialyzer(http://www.erlang.org/doc/man/dialyzer.html)
*2:red + green + blue という書き方もたまに見ます。
*3:char()に属する整数がどんなものか、僕はよく分かってないっす。([追記 date="2007-09-22"]io_lib:char_list/1の挙動を見ると、単に0から255の範囲を“文字”と呼んでいるらしい。[/追記])
*4:レコードでは、要素へのアクセスに名前を使えるという利便性がありますが。