3.11/Chapter 4 ラベルとバリアント

このページは最後に更新されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

last mod. 2009-01-16 (金) 18:01:15

(Chapter written by Jacques Garrigue)

この章では Objective Caml 3 の新機能であるラベルと多相バリアントについての概略を説明します。

Labels

標準ライブラリのモジュールで、名前の終わりに Labels がついているモジュールを見ると、自分で定義した関数にはないような注釈が関数の型についていることがわかります。

#ListLabels.map;;
- : f:('a -> 'b) -> 'a list -> 'b list = <fun>

#StringLabels.sub;;
- : string -> pos:int -> len:int -> string = <fun>

このように name: の形をした注釈を ラベル と言います。これを使うとコードに意味を持たせ、より細かいチェックができるようになり、関数適用の柔軟性があがります。 プログラムの引数をチルダ ~ ではじめると、このように引数に名前をつけることができます。

#let f ~x ~y = x - y;;
val f : x:int -> y:int -> int = <fun>

#let x = 3 and y = 2 in f ~x ~y;;
- : int = 1

型に現れるラベル名と変数名に別の名前を使いたいときは、~name: の形のラベル付けを行います。 関数適用で、引数が変数でないときも、この形を使って適用を行います。

#let f ~x:x1 ~y:y1 = x1 - y1;;
val f : x:int -> y:int -> int = <fun>

#f ~x:3 ~y:2;;
- : int = 1

ラベルは Caml の他の識別子とおなじ文法規則になります。in や to のような予約語はラベルとして使用できません。

正式なパラメータと引数はそれぞれのラベルを元に対応を判断されます 1 。 足りないラベルは空のラベルとして解釈されます。 これによって関数適用の引数を入れ替えることができます。どの引数でも関数に部分適用でき、残っているパラメータで新しい関数を作ります。

#let f ~x ~y = x - y;;
val f : x:int -> y:int -> int = <fun>

#f ~y:2 ~x:3;;
- : int = 1

#ListLabels.fold_left;;
- : f:('a -> 'b -> 'a) -> init:'a -> 'b list -> 'a = <fun>

#ListLabels.fold_left [1;2;3] ~init:0 ~f:(+);;
- : int = 6

#ListLabels.fold_left ~init:0;;
- : f:(int -> 'a -> int) -> 'a list -> int = <fun>

関数の中で複数の引数が同じラベルを持つとき (またはラベルを持たないとき) 、その引数同士の順序を変えることはできず、順序が問題になってきます。ただしそれ以外の引数と入れ替えることはできます。

#let hline ~x:x1 ~x:x2 ~y = (x1, x2, y);;
val hline : x:'a -> x:'b -> y:'c -> 'a * 'b * 'c = <fun>

#hline ~x:3 ~y:2 ~x:5;;
- : int * int * int = (3, 5, 2)

上記の対応規則には例外があります。関数適用が完全であるとき、ラベルは省略可能です。現実的には大抵の関数適用が完全で、ラベルが省略可能です。

#f 3 2;;
- : int = 1

#ListLabels.map succ [1;2;3];;
- : int list = [2; 3; 4]

ここで注意すべきなのが、ListLabels?.fold_left のように結果の型が型変数であるような関数は、完全適用とはみなされないということです。

#ListLabels.fold_left (+) 0 [1;2;3];;
This expression has type int -> int -> int but is here used with type 'a list

関数に高階関数を引数として渡す場合、両者の型のラベルが一致しなければなりません。ラベルを付け足すことも取り外すことも許されません。

#let h g = g ~x:3 ~y:2;;
val h : (x:int -> y:int -> 'a) -> 'a = <fun>

#h f;;
- : int = 1

#h (+);;
This expression has type int -> int -> int but is here used with type
  x:int -> y:int -> 'a

引数が必要ない場合にはワイルドカードパターンを使うことができますが、プレフィックスラベルをつける必要があることに気をつけてください。

#h (fun ~x:_ ~y -> y+1);;
- : int = 3

Optional arguments

ラベル付けした引数はオプションにできるという面白い特徴があります。 オプションのパラメータにするには、オプションでないパラメータのチルダ ~ をクエスチョンマーク ? と置き換えます。関数型のなかのラベルも ? になります。 このようなオプションのパラメータにはデフォルト値を与えることができます。

#let bump ?(step = 1) x = x + step;;
val bump : ?step:int -> int -> int = <fun>

#bump 2;;
- : int = 3

#bump ~step:3 2;;
- : int = 5

オプション引数をとる関数は、最低でも 1 つのラベル付けされない引数をとる必要があります。 これは、オプション引数より後に現れたラベル付けされない引数が適用されたかどうかが、オプション引数が省略されたかどうかの判断基準になっているためです。

#let test ?(x = 0) ?(y = 0) () ?(z = 0) () = (x, y, z);;
val test : ?x:int -> ?y:int -> unit -> ?z:int -> unit -> int * int * int =
  <fun>

#test ();;
- : ?z:int -> unit -> int * int * int = <fun>

#test ~x:2 () ~z:3 ();;
- : int * int * int = (2, 0, 3)

オプションのパラメータはオプションでないパラメータやラベル付けされないパラメータと入れ替えが可能です (ただしそれらのパラメータが一斉に適用される場合) 。本質的に、オプション引数は、別に適用されるラベル付けされない引数と入れ替えることはできません。

#test ~y:2 ~x:3 () ();;
- : int * int * int = (3, 2, 0)

#test () () ~z:1 ~y:2 ~x:3;;
- : int * int * int = (3, 2, 1)

#(test () ()) ~z:1;;
This expression is not a function, it cannot be applied

ここでは、(test () ()) はすでに (0,0,0) となっているので、これ以上適用できません。

オプション引数は実際にはオプション型として実装されています。デフォルト値を与えなかったら、その内部表現 type 'a option = None | Some of 'a にアクセスすることになります。これを利用すると引数があるかないかによって動作を変えることができます。

#let bump ?step x =
   match step with
   | None -> x * 2
   | Some y -> x + y
 ;;
val bump : ?step:int -> int -> int = <fun>

またこれは、関数呼び出しから別の関数呼び出しへオプション引数を中継するのにも使えます。 これを行うには適用する引数を ? で始めます。 ここでクエスチョンマークを使うと、オプション型のオプション引数をラッピングできなくなります。

#let test2 ?x ?y () = test ?x ?y () ();;
val test2 : ?x:int -> ?y:int -> unit -> int * int * int = <fun>

#test2 ?x:None;;
- : ?y:int -> unit -> int * int * int = <fun>

Labels and type inference

ラベルやオプション引数を使うと関数適用の記述力があがりますが、落とし穴もあります。言語の他の部分は完全に推論できるのですが、これらは完全には推論できないのです。

以下の 2 つの例をみるとわかります。

#let h' g = g ~y:2 ~x:3;;
val h' : (y:int -> x:int -> 'a) -> 'a = <fun>

#h' f;;
This expression has type x:int -> y:int -> int but is here used with type
  y:int -> x:int -> 'a

#let bump_it bump x =
   bump ~step:2 x;;
val bump_it : (step:int -> 'a -> 'b) -> 'a -> 'b = <fun>

#bump_it bump 1;;
This expression has type ?step:int -> int -> int but is here used with type
  step:int -> 'a -> 'b

最初の事例は単純です。g は ~y を受け取って次に ~x を受け取りますが、f は ~x を受け取って次に ~y を受け取ります。g の型が x:int -> y:int -> int の順だとすればこれは正しく処理できますが、そうでなければ上記のような型衝突となります。パラメータを同じ順で適用すればとりあえず回避できます。

二つ目の事例はもっと微妙です。引数 bump の型は ?step:int -> int -> int のつもりでやってますが、step:int -> int -> 'a と推論されてしまいます。この 2 つの型は互換性がなく (内部的に普通の引数とオプション引数は異なります) 、bump_it に bump の実体を適用すると型エラーが発生します。

ここでは型推論がどのように動作するかを詳しくは説明しません。上記のプログラムで g や bump の型を正しく導出するには情報が足りなさすぎるということだけ理解してください。つまり関数がどのように適用されているかを見るだけで、引数がオプション引数かどうかや、引数の正しい順序を知る方法はないということです。コンパイラは、オプション引数はないもの、関数適用は正しい順番で行われるもの、と仮定した戦略を採用しています。

オプションのパラメータの問題は、引数 bump に型の注釈をつければちゃんと解決できます。

#let bump_it (bump : ?step:int -> int -> int) x =
   bump ~step:2 x;;
val bump_it : (?step:int -> int -> int) -> int -> int = <fun>

#bump_it bump 1;;
- : int = 3

実際にこのような問題が発生するのは、メンバメソッドがオプション引数を取るようなオブジェクトを使うときです。このためには、オブジェクト引数の型を書くようにするといいです。

関数の引数に、予想される型と異なる型のパラメータを渡そうとすると普通コンパイラはエラーを出しますが、予想される型がラベルなしの関数型で、引数がオプションのパラメータを受け取る関数であるという特定の場合に限り、コンパイラは引数のオプションのパラメータすべてに None を渡して、予想される型にマッチする型に変形しようとします。

#let twice f (x : int) = f(f x);;
val twice : (int -> int) -> int -> int = <fun>

#twice bump 2;;
- : int = 8

この変形は副作用を含む意味論と整合性があります。つまりオプション引数の適用で副作用が発生する場合、その関数が実際に引数として適用されるまで、オプション引数の適用は遅延されます。

Suggestions for labeling

他の名前にも言えることですが、関数のラベルを選ぶのは簡単なことではありません。いいラベル付けは次のようなものです。

  • プログラムの可読性を上げる
  • 覚えやすい
  • できる限り部分適用を行えるようにする ここでは私たちが Objective Caml ライブラリにラベルを付ける際に使った規則を説明します。

「オブジェクト指向」の観点から言うと、関数にはそれぞれ、主となる引数 (オブジェクト) と、その動作に関する引数 (パラメータ) があると考えられます。ラベル順入れ替え可能モードで関数を通して関数の組み合わせをできるように、オブジェクトにはラベルを付けません。オブジェクトの役割は関数自身からはっきりしています。パラメータはラベルを付けて、その本質や役割をわかるようにします。意味の本質と役割が一体化するようなラベルを付けることが理想です。これが不可能なときは役割を重視します。というのは、本質は型で表されていることが多いからです。曖昧な省略は避けるべきです。

ListLabels.map : f:('a -> 'b) -> 'a list -> 'b list
UnixLabels.write : file_descr -> buf:string -> pos:int -> len:int -> unit

本質や役割が同じオブジェクトが複数個ある場合は、すべてラベル付けせずに残します。

ListLabels.iter2 : f:('a -> 'b -> 'c) -> 'a list -> 'b list -> unit

オブジェクトとしてふさわしいものがない場合は、引数すべてにラベルを付けます。

StringLabels.blit :
  src:string -> src_pos:int -> dst:string -> dst_pos:int -> len:int -> unit

しかし、引数が 1 つしかない場合はラベル付けせずに残します。 StringLabels.create : int -> string 引数それぞれの役割がはっきりしている場合に限り、返す型が型変数であり、複数の引数をとる関数にも、この方針を適用します。 このような関数にラベル付けを行うと、ラベルを省略して適用しようとしたとき、やっかいなエラーを引き起こすことがあります (ListLabels?.fold_left でみましたね) 。

ライブラリで使っているラベル名一覧です。

LabelMeaning
f:適用する関数
pos:文字列や配列の位置
len:長さ
buf:バッファとして使う文字列
src:処理を行う元
dst:処理が行われる先
init:イテレータの初期値
cmp:比較関数 e.g. Pervasives.compare
mode:処理モード、フラグのリスト

これはあくまで提案ですが、ラベルの選択は可読性を大きく左右するということを心にとめておいてください。突飛なラベル付けを行うと、プログラムの保守が困難になります。

理想的には、きちんとした名前の関数にきちんとラベル付けをすると、それだけで関数の意味を理解するには十分になります。この情報は OCamlBrowser? やトップレベルの ocaml でわかるので、より詳細な仕様が知りたいときだけドキュメントを調べればいいということになります。

Polymorphic variants

セクション 1.4 で紹介したバリアントは、データ構造やアルゴリズムを組み立てるときにとても便利です。しかしこれはモジュールプログラミングで使っていると、時として柔軟性に欠けています。これは、コンストラクタが個々に名前を予約して、ユニークな型に対応させることに起因します。異なる型に同じ名前を使うことはできません。複数の型の値は、さらにコンストラクタをとって別の型に属すということを考えてみてください。

多相バリアントを使えば、この根本的な前提を取り除きます。つまり、バリアントタグは特にどの型にも属しません。型システムはバリアントタグの使われ方から、そのバリアントタグが許される値かどうかを判断するだけです。バリアントタグは使用する前に型を宣言する必要がありません。バリアント型はその使われ方それぞれから独立に推論されます。

Basic use

プログラム中では、多相バリアントは通常のバリアントと同じように使えます。ただし、名前の前にバッククオート(`)をつける必要があります。

#[`On; `Off];;
- : [> `Off | `On] list = [`On; `Off]

#`Number 1;;
- : [> `Number of int] = `Number 1

#let f = function `On -> 1 | `Off -> 0 | `Number n -> n;;
val f : [< `Number of int | `Off | `On] -> int = <fun>

#List.map f [`On; `Off];;
- : int list = [1; 0]

[>`Off|`On] listは、すくなくとも`Offと`Onにマッチするリストを意味します。(`Off、`Offともに引数なし) [<`On|`Off|`Number of int]は、fが`Off、`On(ともに引数なし)、もしくは`Number n(nはint)を受け取ることを意味します。バリアントの型の内部の>と<は、これらの型がタグを増やしたり減らしたりといった再定義が可能なことを示しています。そのようなものは、暗黙的に型変数を含んでいます。どちらのバリアント型も型中で一度しか現れないので、暗黙的な型変数は表示されません。

上記のバリアント型は多相性を持つので、再定義が可能です。型のアノテーションを書く場合は、開かれていないバリアント型として書かれることが多いので、そのような再定義はできません。これは型の省略(訳注: type t = `On | `Off)でも同様です。そのような型は>や<を含まず、通常のデータ型の記述と同様に単にタグと関連した型を並べてあるだけです。

#type 'a vlist = [`Nil | `Cons of 'a * 'a vlist];;
type 'a vlist = [ `Cons of 'a * 'a vlist | `Nil]

#let rec map f : 'a vlist -> 'b vlist = function
   | `Nil -> `Nil
   | `Cons(a, l) -> `Cons(f a, map f l)
 ;;
val map : ('a -> 'b) -> 'a vlist -> 'b vlist = <fun>

Advanced use

多相バリアントの型チェックは微妙なもので、型情報が通常よりも複雑になってしまう式も存在します。

Type-checking polymorphic variants is a subtle thing, and some expressions may result in more complex type information.

#let f = function `A -> `C | `B -> `D | x -> x;;
val f : ([> `A | `B | `C | `D] as 'a) -> 'a = <fun>

#f `E;;
- : _[> `A | `B | `C | `D | `E] = `E

Here we are seeing two phenomena. First, since this matching is open (the last case catches any tag), we obtain the type [> `A | `B] rather than [< `A | `B] in a closed matching. Then, since x is returned as is, input and return types are identical. The notation as 'a denotes such type sharing. If we apply f to yet another tag `E, it gets added to the list.

#let f1 = function `A x -> x = 1 | `B -> true | `C -> false
 let f2 = function `A x -> x = "a" | `B -> true ;;
val f1 : [< `A of int | `B | `C] -> bool = <fun>
val f2 : [< `A of string | `B] -> bool = <fun>

#let f x = f1 x && f2 x;;
val f : [< `A of string & int | `B] -> bool = <fun>

Here f1 and f2 both accept the variant tags `A and `B, but the argument of `A is int for f1 and string for f2. In f's type `C, only accepted by f1, disappears, but both argument types appear for `A as int & string. This means that if we pass the variant tag `A to f, its argument should be both int and string. Since there is no such value, f cannot be applied to `A, and `B is the only accepted input.

Even if a value has a fixed variant type, one can still give it a larger type through coercions. Coercions are normally written with both the source type and the destination type, but in simple cases the source type may be omitted.

#type 'a wlist = [`Nil | `Cons of 'a * 'a wlist | `Snoc of 'a wlist * 'a];;
type 'a wlist = [ `Cons of 'a * 'a wlist | `Nil | `Snoc of 'a wlist * 'a]

#let wlist_of_vlist  l = (l : 'a vlist :> 'a wlist);;
val wlist_of_vlist : 'a vlist -> 'a wlist = <fun>

#let open_vlist l = (l : 'a vlist :> [> 'a vlist]);;
val open_vlist : 'a vlist -> [> 'a vlist] = <fun>

#fun x -> (x :> [`A|`B|`C]);;
- : [< `A | `B | `C] -> [ `A | `B | `C] = <fun>

You may also selectively coerce values through pattern matching.

#let split_cases = function
   | `Nil | `Cons _ as x -> `A x
   | `Snoc _ as x -> `B x
 ;;
val split_cases :
  [< `Cons of 'a | `Nil | `Snoc of 'b] ->
  [> `A of [> `Cons of 'a | `Nil] | `B of [> `Snoc of 'b]] = <fun>

When an or-pattern composed of variant tags is wrapped inside an alias-pattern, the alias is given a type containing only the tags enumerated in the or-pattern. This allows for many useful idioms, like incremental definition of functions.

#let num x = `Num x
 let eval1 eval (`Num x) = x
 let rec eval x = eval1 eval x ;;
val num : 'a -> [> `Num of 'a] = <fun>
val eval1 : 'a -> [ `Num of 'b] -> 'b = <fun>
val eval : [ `Num of 'a] -> 'a = <fun>

#let plus x y = `Plus(x,y)
 let eval2 eval = function
   | `Plus(x,y) -> eval x + eval y
   | `Num _ as x -> eval1 eval x
 let rec eval x = eval2 eval x ;;
val plus : 'a -> 'b -> [> `Plus of 'a * 'b] = <fun>
val eval2 : ('a -> int) -> [< `Num of int | `Plus of 'a * 'a] -> int = <fun>
val eval : ([< `Num of int | `Plus of 'a * 'a] as 'a) -> int = <fun>

To make this even more confortable, you may use type definitions as abbreviations for or-patterns. That is, if you have defined type myvariant = [`Tag1 int | `Tag2 bool], then the pattern #myvariant is equivalent to writing (`Tag1(_ : int) | `Tag2(_ : bool)).

Such abbreviations may be used alone,

#let f = function
   | #myvariant -> "myvariant"
   | `Tag3 -> "Tag3";;
val f : [< `Tag1 of int | `Tag2 of bool | `Tag3] -> string = <fun>

or combined with with aliases.

#let g1 = function `Tag1 _ -> "Tag1" | `Tag2 _ -> "Tag2";;
val g1 : [< `Tag1 of 'a | `Tag2 of 'b] -> string = <fun>

#let g = function
   | #myvariant as x -> g1 x
   | `Tag3 -> "Tag3";;
val g : [< `Tag1 of int | `Tag2 of bool | `Tag3] -> string = <fun>
** Weaknesses of polymorphic variants

After seeing the power of polymorphic variants, one may wonder why they were added to core language variants, rather than replacing them.

The answer is two fold. One first aspect is that while being pretty efficient, the lack of static type information allows for less optimizations, and makes polymorphic variants slightly heavier than core language ones. However noticeable differences would only appear on huge data structures.

More important is the fact that polymorphic variants, while being type-safe, result in a weaker type discipline. That is, core language variants do actually much more than ensuring type-safety, they also check that you use only declared constructors, that all constructors present in a data-structure are compatible, and they enforce typing constraints to their parameters.

For this reason, you must be more careful about making types explicit when you use polymorphic variants. When you write a library, this is easy since you can describe exact types in interfaces, but for simple programs you are probably better off with core language variants.

Beware also that certain idioms make trivial errors very hard to find. For instance, the following code is probably wrong but the compiler has no way to see it.

#type abc = [`A | `B | `C] ;;
type abc = [ `A | `B | `C]

#let f = function
   | `As -> "A"
   | #abc -> "other" ;;
val f : [< `A | `As | `B | `C] -> string = <fun>

#let f : abc -> string = f ;;
val f : abc -> string = <fun>

You can avoid such risks by annotating the definition itself.

#let f : abc -> string = function
   | `As -> "A"
   | #abc -> "other" ;;
Warning: this match case is unused.
val f : abc -> string = <fun>

新規 編集 添付