4.1 ラベル

4.1.1. 省略可能引数
4.1.2. ラベルと型推論
4.1.3. ラベル付けに関する提案

標準ライブラリのモジュールで、名前の終わりに 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 の他の識別子とおなじ文法規則になり、(into のような)予約語はラベルとして使用できません。

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

#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
    

4.1.1 省略可能引数

ラベル付けした引数には省略可能にできるという面白い特徴があります。 省略可能引数にするには、省略可能でない引数の ~? と置き換えます。関数型のなかのラベルも ? になります。 このような省略可能引数にはデフォルト値を与えることができます。

#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>
      

4.1.2 ラベルと型推論

ラベルや省略可能引数を使うと関数適用の記述力が増しますが、落とし穴もあります。言語の他の部分のように、完全には推論できないのです。

以下の 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_itbump の実体を適用すると型エラーが発生します。

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

省略可能引数の問題は、引数 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
      

この変形は副作用を含む意味論と整合性があります。つまり省略可能引数の適用で副作用が発生する場合、その関数が実際に引数に適用されるまで、副作用は遅延されます。

4.1.3 ラベル付けに関する提案

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

  1. プログラムの可読性を上げる
  2. 覚えやすい
  3. できる限り部分適用を行えるようにする

ここでは私たちが 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 で見たような、わけのわからないエラーメッセージが出ることがあります。

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

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

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

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



[2] これは Objective Caml 3.00 から 3.02 の commuting label mode に、全適用(total application)に関する柔軟性を加えたものに対応します。いわゆる classic mode(-nolabels オプション)は一般的な用途では推奨されません。