OCaml は関数の引数に「ラベル (label) 」を付けることができます。これをラベル付き引数といいます。引数の個数が多い関数は、その順番を覚えるのが大変です。ラベル付き引数はラベル名で引数を指定できるので、引数の順番を覚える必要はありません。つまり、引数の順番を変えることができるのです。これは、カリー化関数で部分適用を行うときにとても役に立ちます。
OCaml の標準ライブラリには Labels で終わるモジュール、たとえば、ArrayLabels や ListLabels などがあります。これらのモジュールを使うと、Array や List の関数をラベル付き引数で使用することができます。
ラベル付き引数は次のように指定します。
~ラベル名:パターン
実引数は ~ラベル名:実引数 と指定します。標準モジュールで使用されている主なラベル名を表に示します。
ラベル名 | 意味 |
---|---|
f | 関数 |
pos | 位置 |
len | 長さ |
buf | バッファ |
src | 操作対象元 |
dst | 操作対象先 |
init | 初期値 |
cmp | 比較関数 |
mode | モード |
簡単な例題としてマップ関数をラベル付き引数でプログラムしてみましょう。次のリストを見てください。
リスト 1 : ラベル付き引数 (1) let rec map ~f:func = function [] -> [] | (x::xs) -> func x :: map ~f:func xs
ラベル名とパターンが一致する場合、パターンを省略することができます。プログラムは次のようになります。
リスト 2 : ラベル付き引数 (2) let rec map ~f = function [] -> [] | (x::xs) -> f x :: map ~f xs
簡単な実行例を示します。
# map ~f:(fun x -> x * x) [1; 2; 3; 4; 5];; - : int list = [1; 4; 9; 16 ;25] # map [1; 2; 3; 4; 5] ~f:(fun x -> x * x);; - : int list = [1; 4; 9; 16 ;25]
もう一つ簡単な例を示しましょう。畳み込み関数 fold_left と fold_right をラベル付き引数でプログラムします。次のリストを見てください。
リスト 3 : 畳み込み関数 let rec fold_right ~f ls ~init = match ls with [] -> init | x::xs -> f x (fold_right ~f xs ~init) let rec fold_left ~f ls ~init = match ls with [] -> init | x::xs -> fold_left ~f xs ~init:(f init x)
これらの関数を使うと、length と reverse は次のように定義することができます。
# let length ls = fold_right ~f:(fun x y -> y + 1) ~init:0 ls val length : 'a list -> int = <fun> # let reverse ls = fold_left ~f:(fun x y -> y :: x) ~init:[] ls val reverse : 'a list -> 'a list = <fun>
引数の順番を変更できるのはラベル付き引数だけで、通常の引数は順番を変更することはできません。次の例を見てください。
リスト 4 : リストの生成 (1) let rec tabulate ~f s e = if s > e then [] else f s :: tabulate ~f (s + 1) e
関数 tabulate は整数 s から e までの値を関数 f に適用し、その結果をリストに格納して返します。関数 f はラベルを指定することで引数のどの位置にでも指定することができますが、引数 s と e の順番は変えることができません。ラベルなしの最初の実引数が s になり、次の実引数が e になります。
簡単な実行例を示しましょう。
# tabulate ~f:(fun x -> x * x) 1 5;; - : int list = [1; 4; 9; 16; 25] # tabulate 1 ~f:(fun x -> x * x) 5;; - : int list = [1; 4; 9; 16; 25] # tabulate 1 5 ~f:(fun x -> x * x);; - : int list = [1; 4; 9; 16; 25]
なお、引数の順番が関数定義と同じで、すべての引数が与えられている場合に限り、ラベルを省略することができます。次の例を見てください
# tabulate (fun x -> x * x) 1 5;; - : int list = [1; 4; 9; 16; 25]
ただし、関数によってはラベルを省略するとエラーになる場合があります。たとえば、fold_left や fold_right がそうです。次の例を見てください。
# fold_left (fun x y -> x + y) 0 [1;2;3;4;5];; Characters 10-28: fold_left (fun x y -> x + y) 0 [1;2;3;4;5];; ^^^^^^^^^^^^^^^^^^ This expression should not be a function, the expected type is 'a list
これは、関数の返り値のデータ型によっては、すべての引数が与えられていないと OCaml が判断するため、最初の実引数 (関数) をラベルなしの引数 (リスト) に割り当てようとするために発生するようです。詳しいことは 参考文献 [1] をお読みください。
OCaml のラベル付き引数は、デフォルトの値を設定することができます。これを「オプション引数」といいます。関数を呼び出すとき、オプション引数は省略することができます。その場合、オプション引数の値はデフォルトの値が使用されます。
オプション引数は次のように指定します。
?ラベル名:(パターン:型=式)
:型 の部分は省略することができます。また、ラベル付き引数と同様に、ラベル名とパターンが同じ場合は、次のように指定することができます。
?(パターン=式)
実引数の指定方法はラベル付き引数と同じです。簡単な例を示しましょう。次のリストを見てください。
リスト 5 : リストの生成 (2) let rec iota ?(step=1) s e = if s > e then [] else s :: iota ~step (s + step) e
関数 iota は整数 s から e までの値を step 間隔でリストに格納して返します。step はオプション引数として宣言しているので、iota を呼び出すとき step の値を省略することができます。その場合、step の値は 1 になります。
実際に iota を定義すると、関数の型は次のように表示されます。
val iota : ?step:int -> int -> int -> int list = <fun>
このように、step の前にオプション引数であることを示す ? が付きます。
それでは簡単な実行例を示しましょう。
# iota 1 5;; - : int list = [1; 2; 3; 4; 5] # iota 1 10 ~step:2;; - : int list = [1; 3; 5; 7; 9]
iota 1 5 は step が省略されているので返り値は [1; 2; 3; 4; 5] になります。次の例では step が 2 なので、数値の間隔は 2 になります。オプション引数はラベル付き引数と同様に順番を変えることができます。
ただし、最後の引数をオプション引数にすると、オプション引数を省略することができなくなります。たとえば、次のように iota 定義すると、警告が表示されます。
# let rec iota s e ?(step=1) = if s > e then [] else iota (s + step) e ~step;; Characters 24-25: Warning X: this optional argument cannot be erased. let rec iota s e ?(step=1) = ^ val iota : int -> int -> ?step:int -> 'a list = <fun> # iota 1 10;; - : ?step:int -> 'a list = <fun>
このように、関数を定義することはできますが、オプション引数 step を省略することはできません。オプション引数は他の引数よりも前に定義してください。もしも、すべての引数をオプション引数にする場合は、最後に unit を渡すようにしてください。簡単な例を示しましょう。
# let foo ?(a=1) ?(b=2) () = Printf.printf "(%d,%d)\n" a b;; val foo : ?a;int -> ?b;int -> unit -> unit = <fun> # foo ();; (1,2) - : unit = ()
オプション引数は多数の引数を必要とする関数を定義するときに使うと便利です。たとえば、GUI ライブラリ LablTk ではオプション引数を多用しています。
OCaml のヴァリアントはとても便利な機能ですが、一つのコンストラクタを複数のヴァリアント型で使用できない、という制限があります。この制限を取り払ったものが「多相ヴァリアント (polymorphic variant) 」です。今回は多相ヴァリアントの基本について簡単に説明します。
多相ヴァリアントの場合、データ型を定義しなくてもコンストラクタを使用することができます。多相ヴァリアントのコンストラクタは名前の前にバッククオート ( ` ) を付けて表します。簡単な例を示しましょう。
# `Apple;; - : [> `Apple ] = `Apple # 'Orange;; - : [> `Orange ] = `Orange # [`Apple; `Orange];; - : [> `Apple | `Orange ] list = [`Apple; `Orange]
多相ヴァリアントのデータ型は [ ... ] で表します。また、[ の後ろに > または < が付く場合があります。この違いはあとで説明します。
もちろん、多相型ヴァリアントでもコンストラクタに引数を渡すことができます。
# `Foo 1;; - : [> `Foo of int ] = `Foo 1 # `Foo "abc";; - : [> `Foo of string ] = `Foo "abc" # `Foo (1, "abc");; - : [> `Foo of int * string ] = `Foo (1, "abc")
このように、同じコンストラクタでも異なる引数の型を指定することができます。ただし、引数の型が異なる同名のコンストラクタをリストに混在させることはできません。
# [`Foo 1; `Foo "abc"] ... エラー (省略) ...
この場合、異なるデータ型として扱われるため、リストに格納することはできません。つまり、同じ型が必要なところでは混在させることができない、というわけです。次に示すように、組にすることは可能です。
# (`Foo 1, `Foo "abc");; - : [> `Foo of int ] * [> `Foo of string ] = (`Foo 1, `Foo "abc")
また、ヴァリアントと同様にパターンマッチングでも多相ヴァリアントを使用することができます。次の例を見てください。
リスト 6 : 合計値を求める let rec sum_of_number ?(a=0) ?(b=0.0) = function [] -> (a, b) | `Int x :: xs -> sum_of_number ~a:(a + x) ~b xs | `Float x :: xs -> sum_of_number ~a ~b:(b +. x) xs
関数 sum_of_number は整数と実数の合計を求め、組 (int * float) にして返します。`Int が整数を表し、`Float が実数を表します。オプション引数 a, b を累積変数として使い、a に整数の合計値、b に実数の合計値を求めます。`Int x :: xs と `Float x :: xs がパターンです。リストの先頭の要素が `Int の場合、2 番目の定義とマッチングして、x の値は整数になります。要素が `Float の場合は 3 番目の定義とマッチングして、x の値は実数になります。
実際に sum_of_number を定義すると、データ型は次のように表示されます。
val sum_of_number : ?a:int -> ?b:float -> [< `Float of float | `Int of int ] list -> int * float = <fun>
簡単な実行例を示します。
# sum_of_number [`Int 1; `Int 2; `Float 1.2; `Int 3; `Float 4.5];; - : int * float = (6, 5.7)
それでは、[> ...] と [< ...] の違いについて説明します。[> ...] で表されるデータ型は、角カッコの中のコンストラクタだけではなく、他の任意のコンストラクタを含めることができます。次の例を見てください。
# let a = [`A];; val a : [> `A ] list = [`A] # let b = `B :: a;; val b : [> `A | `B ] list = [`B; `A] # let c = `C :: b;; val c : [> `A | `B | `C ] list = [`C; `B; `A] # `A 10 :: a;; Characters 9-10: `A 10 :: a;; ^ This expression has type [> `A ] list but is here used with type [> `A of int ] list Types for tag `A are incompatible
`A をリストに格納すると、そのデータ型は [> `A] list になります。データ型は [> ...] なので、このリストに任意の多相ヴァリアントのコンストラクタを追加することができます。`B を追加すると型は [> `A | `B ] list になり、さらに `C を追加すると [> `A | `B | `C ] list になります。
ただし、角カッコの中のコンストラクタは、そのデータ型しか受け付けません。たとえば [> `A] list の場合、`A を追加することはできますが、`A 10 のように異なるデータ型 (`A of int) を追加することはできません。つまり、角カッコの中でコンストラクタのデータ型を制限しているわけです。
また、データ型を [ `A ] list に指定すると、要素が `A のリストになり、[ `A | `B ] list であれば、要素が `A または `B のリストになります。この場合、他のコンストラクタを追加することはできません。簡単な例を示します。
# let a = ([`A]: [`A] list);; val a : [ `A ] list = [`A] # `A :: a;; - : [ `A ] list = [`A; `A] # `B :: a;; Characters 6-7: `B :: a;; ^ This expression has type [ `A ] list but is here used with type [> `B ] list The first variant type does not allow tag(s) `B
これに対して、[< ...] が表すデータ型は、角カッコの中に定義されているコンストラクタからなるデータ型を表します。たとえば、[< `A | `B ] はデータ型が [ `A ] でも [ `B ] でも [ `A | `B ] でも受け付けます。他のデータ型は受け付けません。
簡単な例を示しましょう。リストに格納された `A と `B の個数を返す関数 foo を作ります。
リスト 7 : `A と `B の個数を求める (1) let rec foo ?(a=0) ?(b=0) ls = match ls with [] -> (a, b) | `A :: xs -> foo ~a:(a + 1) ~b xs | `B :: xs -> foo ~a ~b:(b + 1) xs
foo を定義すると、データ型は次のように表示されます。
val foo ; ?a:int -> ?b:int -> [< `A | `B ] list -> int * int = <fun>
リストのデータ型は [< `A | `B ] list なので、 [ `A ] list でも [ `B ] list でも [ `A | `B ] list でも受け付けます。次の例を見てください。
# let a = ([`A; `A; `A]: [`A] list);; val a : [ `A ] list = [`A; `A; `A] # let b = ([`B; `B; `B]: [`B] list);; val b : [ `B ] list = [`B; `B; `B] # let c = ([`A; `B; `A]: [`A | `B] list);; val c : [ `A | `B ] list = [`A; `B; `A] # foo a;; - : int * int = (3, 0) # foo b;; - : int * int = (0, 3) # foo c;; - : int * int = (2, 1)
変数 a には [`A] list、b には [`B] list、c には [`A | `B] list をセットします。これらのリストはデータ型を規定していることに注意してください。それでも、これらのリストを foo に渡すと `A と `B の個数を求めることができます。
もしも、次のように foo の引数のデータ型を規定すると、そのデータ型しか渡すことができません。
リスト 8 : `A と `B の個数を求める (2) let rec foo1 ?(a=0) ?(b=0) (ls: [`A | `B] list) = match ls with [] -> (a, b) | `A :: xs -> foo1 ~a:(a + 1) ~b xs | `B :: xs -> foo1 ~a ~b:(b + 1) xs
foo1 を定義すると、データ型は次のように表示されます。
val foo1 ; ?a:int -> ?b:int -> [ `A | `B ] list -> int * int = <fun>
この場合、[`A] list や [`B] list を渡すとエラーになります。ただし、リストの型が [> `A ] list や [> `B ] list であれば、foo1 に渡すことができます。
# let a1 = [`A; `A; `A];; val a1 : [> `A ] list = [`A; `A; `A] # foo1 a1;; - : int * int = (3, 0) # let b1 = [`B; `B; `B];; val b1 : [> `B ] list = [`B; `B; `B] # foo1 b1;; - : int * int = (0, 3)
今度は `A, `B 以外のコンストラクタも数える関数 bar を作りましょう。次のリストを見てください。
リスト 9 : `A と `B とその他のコンストラクタの個数を求める let rec bar ?(a=0) ?(b=0) ?(c=0) ls = match ls with [] -> (a, b, c) | `A :: xs -> bar ~a:(a + 1) ~b ~c xs | `B :: xs -> bar ~a ~b:(b + 1) ~c xs | _ :: xs -> bar ~a ~b ~c:(c + 1) xs
bar を定義すると、データ型は次のように表示されます。
val bar : ?a:int -> ?b:int -> ?c:int -> [> `A | `B ] list -> int * int * int = <fun>
引数の型が [> `A | `B ] list に変わっていることに注意してください。bar は `A, `B 以外のコンストラクタも受け付けるので、データ型が [< ...] から [> ...] になるのです。簡単な実行例を示しましょう。
# bar a1;; - : int * int * int = (3, 0, 0) # bar b1;; - : int * int * int = (0, 3, 0) # bar c;; - : int * int * int = (2, 1, 0) # let d = ([`A; `B; `C]: [`A | `B| `C]);; val d : [`A | `B | `C] list = [`A; `B; `C] # bar d;; - : int * int * int = (1, 1, 1)
データ型が [> `A | `B ] なので、[`A] list や [`B] list を bar に渡すとエラーになります。ただし、[> `A] list や [> `B] list は大丈夫です。また、[`A | `B] list や [`A | `B | `C] list も渡すことができます。
多相ヴァリアントを使うと、関数定義を変更せずに機能を拡張できる場合があります。たとえば、多相ヴァリアントで図形の面積を求める関数 area を作ってみましょう。次のリストを見てください。
リスト 10 : 図形の面積を求める let pi = 3.14159265 (* 面積を求める *) let area = function `Circle r -> r *. r *. pi | `Triangle (a, b) -> a *. b /. 2.0 | `Rectangle (w, h) -> w *. h
`Circle が円、`Triangle が三角形、`Rectangle が長方形を表します。area を定義すると、型は次のように表示されます。
val area : [< `Circle of float | `Rectangle of float * float | `Triangle of float * float ] -> float = <fun>
実行例を示します。
# area (`Circle 10.0);; - : float = 314.159265 # area (`Triangle (10.0, 10.0));; - : float = 50. # area (`Rectangle (10.0, 10.0));; - : float = 100.
次は、この area に新しい図形 `Square (正方形) を追加することを考えます。多相ヴァリアントを使うと、area を修正しなくても新しい図形を追加することができます。次のリストを見てください。
リスト 11 : 図形の面積を求める (2) type figure = [ `Circle of float | `Triangle of float * float | `Rectangle of float * float ] let area = function `Square edge -> edge *. edge | #figure as x -> area x
新しい関数 area を定義し、そこから元の関数 area を呼び出します。たとえば、対話モードでプログラムを読み込むと、変数や関数の定義は対話モードの環境に追加されます。対話モードの環境を「トップレベルの環境」といいます。トップレベルの環境には、あらかじめ定義されている関数や変数があます。変数や関数を環境に追加するとき、同じ名前が存在している場合はどうなるのでしょうか。次の例を見てください。
# let a = 10;; val a : int = 10 # a;; - : int = 10 # let a = "foo";; val a : string = "foo" # a;; - : string = "foo"
let による変数の定義は、変数束縛を生成して環境に追加するだけです。変数束縛を変数名と値の組で、環境を連想リストで表すとわかりやすいと思います。最初、環境は空リストとします。次の図を見てください。
環境 --------------------------------------- [ ] let a = 10 ==> [(a, 10)] let a = "foo" ==> [(a, "foo"); (a, 10)] 図 1 : トップレベルの環境
let a = 10 は変数束縛 (a, 10) を生成して環境に追加します。環境は [ (a, 10) ] になります。環境から値を求める場合は、連想リストの探索と同じです。連想リストの先頭から変数 a を探し、最初に見つけた変数束縛の値を返します。この場合、変数 a の値は 10 になります。
次の let a = "foo" も同様に、(a, "foo") を生成して環境に追加します。このとき、連想リストの先頭に追加するので、環境は [ (a, "foo"); (a, 10) ] になります。変数 a の値を求めると、最初に見つかる変数束縛は (a, "foo") なので、変数 a の値は "foo" になります。
結果だけを見ると、変数 a の値を書き換えているように見えますが、実際は新しい変数束縛を生成して環境に追加しているだけなのです。環境には前の変数束縛も残っています。しかしながら、追加した変数束縛によって隠されてしまうので、前の変数束縛にアクセスすることはできません。
これは関数定義の場合も同じです。新しい area を定義する場合、古い area が書き換えられることはありません。新しい area は再帰関数ではないので、その中で関数 area を呼び出すと、古い area の定義を呼び出すことになります。そして、新しい area が定義されると変数束縛が追加されるので、対話モードや他の関数から area を呼び出すと、新しい関数 area の定義を呼び出すことになるわけです。
元の関数を呼び出すとき、`Circle, `Triangle, `Rectangle とマッチするパターンを記述する必要があります。このとき、OR パターンでプログラムしてもいいのですが、type で型を宣言して、その名前の前に # を付けると簡単です。type で型 figure を宣言すると、`Circle, `Triangle, `Rectangle は #figure でパターンマッチングすることができます。
実際に、figure と area を定義すると、型は次のように表示されます。
type figure = [ `Circle of float | `Rectangle of float * float | `Triangle of float * float ] val area : [< `Circle of float | `Rectangle of float * float | `Square of float | `Triangle of float * float ] -> float = <fun>
それでは、`Square の実行例を示します。
# area (`Square 10.);; - : float = 100.
同様に、新しい図形 `Trapezoid (台形) を追加することができます。次のリストを見てください。
リスト 12 : 図形の面積を求める (3) type figure1 = [ figure | `Square of float ] let area = function `Trapezoid (a, b, h) -> (a +. b) *. h /. 2.0 | #figure1 as x -> area x
figure に `Square of float を追加するだけで新しい型 figure1 を定義することができます。あとは、`Trapezoid の処理を追加するだけです。実際に figure1 と area を定義すると、型は次のように表示されます。
type figure1 = [ `Circle of float | `Rectangle of float * float | `Square of float | `Triangle of float * float ] val area : [< `Circle of float | `Rectangle of float * float | `Square of float | `Trapezoid of float * float * float | `Triangle of float * float ] -> float = <fun>
実行例を示しましょう。
# area (`Trapezoid (1., 2., 3.));; - : float = 4.5
このほかにも、多相ヴァリアントは再帰的なデータ構造も定義できますし、より高度な使い方も可能です。ただし、正直にいいますが、多相ヴァリアントのデータ型は複雑で、M.Hiroi のような初心者が使いこなすにはちょっと難しいですね。今後の課題にしたいと思います。