前回は,OCamlでライブラリやプログラムを構築するための仕組みである「モジュール・システム」について述べた。では,それらのプログラムが処理するデータ(値)とその型は,どのように定義できるのだろうか?
値の組(タップル)
まず,整数や浮動小数といったプリミティブ(原始的値)は,すでに何度も登場したのでよいと思う。また,前回も使用したが,OCamlを始めとする多くの関数型言語には,複数の値を組み合わせて一つの値とするタップル(組)というデータ構造がある。特に,二つの値の組のことをペア(二つ組)という。
OCamlでは,値をコンマで区切って並べることにより組を表す。
# (1.2, 3.45) ;; - : float * float = (1.2, 3.45) # (123, 4.5, "abc") ;; - : int * float * string = (123, 4.5, "abc")
ちなみに,組の前後には通常は括弧を付けるが,OCamlでは実は省略することもできる。
# 123, 4.5, "abc" ;; - : int * float * string = (123, 4.5, "abc")
しかし,他の構文との優先順位などで混乱することも多いので,むやみに省略しないほうが無難だろう。
上の出力結果を見てもわかるように,組の型は,かけ算の記号でもあるアスタリスクを流用して「型1 * 型2 * 型3 * …」のように表される。組の一つ目の値が型1を持ち,二つ目の値が型2を持ち,三つ目の値が型3を持ち…,という意味だ(かけ算の記号を使う理由については後で説明する)。
組の中から値を取り出すには,パターン・マッチングという仕組みを使う。次の例では,組xを(a, b, c)というパターンにマッチさせることにより,xの中の値123, 4.5, "abc"を取り出して,それらをa, b, cと置いている。
# let x = (123, 4.5, "abc") ;; (* 組xを定義 *) val x : int * float * string = (123, 4.5, "abc") # match x with (a, b, c) -> (* 組xから値a, b, cを取り出す *) Printf.printf "a = %d, b = %f, c = %s\n" a b c ;; a = 123, b = 4.500000, c = abc - : unit = ()
組やパターンはネストできる。つまり,下のように「組の組」を作ったり,その中身をパターン・マッチングで一気に取り出したりできる。
# let y = (x, x) ;; (* 組の組を作る *) val y : (int * float * string) * (int * float * string) = ((123, 4.5, "abc"), (123, 4.5, "abc")) # match y with ((a1, b1, c1), (a2, b2, c2)) -> (* その中身を一気に取り出す *) Printf.printf "a1 = %d, b1 = %f, c1 = %s\n" a1 b1 c1; Printf.printf "a2 = %d, b2 = %f, c2 = %s\n" a2 b2 c2 ;; a1 = 123, b1 = 4.500000, c1 = abc a2 = 123, b2 = 4.500000, c2 = abc - : unit = ()
パターン・マッチングにおいて,値を無視したいときは,アンダースコア記号を使う。これは"don't care"パターンと呼ばれ,一部の値だけ取り出したいときなどによく利用する。
# match x with (a, _, _) -> (* 三つ組xの第1要素だけ取り出す *) Printf.printf "a = %d\n" a ;; a = 123 - : unit = ()
要素に名前のついた組(レコード)
上で述べたように,組を使えば複数の値を一つにまとめることが可能だ。しかし,例えば文字列の三つ組で「氏名と住所と電話番号」を表しても,単に「string * string * string」という型になってしまい,どのstringが何を表しているのかわかりにくい。
そうしたときには,組の要素に名前(ラベル)をつけた値であるレコードを使えばよい。レコードの構文は次の通りだ。
{ ラベル名1 = 値1; ラベル名2 = 値2; ラベル名3 = 値3; ... }
ただし,OCamlでは,レコードを使うためには,あらかじめ以下のようにレコードの型を宣言しておく必要がある。
type 型名 = { ラベル名1 : 型1; ラベル名2 : 型2; ラベル名3 = 型3; ... }
ちなみに,このようなレコードについての制限をきちんと解消したML系言語の処理系としては,東北大学の大堀淳教授らが開発しているSML#がある(なお,SML#はC#よりずっと以前から存在しており,.NETと直接の関係はない)。
組と同じく,レコードの中の値も,パターン・マッチングを使って取り出すことができる。以下にレコードの定義と使い方の例を示す。
# (* レコード型の定義 *) type person = { name : string; addr : string; phone : string } ;; type person = { name : string; addr : string; phone : string; } # (* レコード値の定義 *) let r = { name = "Nikkei Taro"; addr = "Minatoku Shirokane 1-17-3"; phone = "03-xxx-yyyy" } ;; val r : person = {name = "Nikkei Taro"; addr = "Minatoku Shirokane 1-17-3"; phone = "03-xxx-yyyy"} # (* レコードのパターン・マッチング *) match r with { name = n; addr = _; phone = p } -> Printf.printf "%s: %s\n" n p ;; Nikkei Taro: 03-xxx-yyyy - : unit = ()
値の場合分け(バリアント)
上で述べたように,組やレコードは「intとfloatとstring」ないし「nameとaddrとphone」のように,複数の値の組み合わせを表す。これに(ある意味で)対応するデータ構造として,「XまたはYまたはZ」のように,複数の型の場合分けを表すバリアントがある(ちなみに,第5回で紹介した多相バリアントは,通常のバリアントの拡張になっている)。
例えば,何らかの処理が成功したら整数値を返し,そうでなければエラー文字列を返す,という関数を定義したかったら,以下のようなバリアント型iresultを利用できる。
# type iresult = ISuccess of int | IFailure of string ;; type iresult = ISuccess of int | IFailure of string # let div x y = if y = 0 then IFailure "division by zero" else ISuccess (x / y) ;; val div : int -> int -> iresult = <fun>
実際に関数を呼び出してみると,次のようになる。
# div 123 4 ;; - : iresult = ISuccess 30 # div 56 0 ;; - : iresult = IFailure "division by zero"
ここで,「ISuccess」や「IFailure」は,バリアントがどの値を取っているかを表すタグで,コンストラクタとも呼ばれる。ISuccessやIFailureなどのタグに,30や"division by zero"などの値を与えると,バリアント型の値を作ることができるからだ。なお,OCamlでは,コンストラクタの頭文字は大文字でなければならない。
一般に,バリアント型は以下の構文で定義できる。
type 型名 = タグ1 of 型1 | タグ2 of 型2 | タグ3 of 型3 | ...
ただし,タグだけ必要で,値が不要の場合は,of以降を省略することもできる。例えば,曜日を表すバリアント型は次のように定義できる。
# type day = Sun | Mon | Tue | Wed | Thu | Fri | Sat ;; type day = Sun | Mon | Tue | Wed | Thu | Fri | Sat
論理値を表すbool型の値も,実は下のように定義されるバリアントとみなすことができる。
type bool = true | false
ただし,OCamlではbool型はプリミティブになっており,上のような定義は不要である。
バリアント型の値を処理したいときには,組の処理と同様に,パターン・マッチングを用いる。例えば,与えられた曜日が平日かどうかを返す関数は,次のように書ける。
# let weekday d = match d with Sun -> false | Mon -> true | Tue -> true | Wed -> true | Thu -> true | Fri -> true | Sat -> false ;; val weekday : day -> bool = <fun>
あるいは,以下のように,複数のパターンを縦棒で区切り,一つにまとめて書くこともできる。
# let weekday d = match d with Sun | Sat -> false | Mon | Tue | Wed | Thu | Fri -> true ;; val weekday : day -> bool = <fun>
さらに,OCamlのパターン・マッチングは前から順に行われるので,先に出てきたdon't careパターンを使って,下のように書いてもよい。
# let weekday d = match d with Sun | Sat -> false | _ -> true ;; val weekday : day -> bool = <fun>
実際に,weekday関数をday型の値に適用してみると次のようになる。
# weekday Wed ;; - : bool = true # weekday Sat ;; - : bool = false
先のiresult型のように,パラメータがある場合も同様だ。例えば,iresult型の値をprintする関数は,以下のように定義できる(もっとも,対話環境では元から結果の値が表示されるので,printすること自体にはあまり意味がないが)。
# let print_iresult r = match r with ISuccess i -> Printf.printf "%d\n" i | IFailure s -> Printf.printf "error: %s\n" s ;; val print_iresult : iresult -> unit = <fun> # print_iresult (div 123 4) ;; 30 - : unit = () # print_iresult (div 56 0) ;; error: division by zero - : unit = ()