この記事はML Advent Calendar 2021 5日目の記事です。
OCamlのラベル付き引数を紹介します。
前置き
静的型付き言語の利点のひとつとして、型を見ればなんとなく処理の内容の想像がつくという点がある。
'a -> 'a list -> bool
であればリストに値が含まれるか判定する関数だろうし、('a -> bool) -> 'a list -> 'a option
であればリストから述語を満たす値を見つける関数だろう。string -> string list -> string
だと探索関数っぽさもあるけれど、文字列のリストの結合関数かなという気がする(ちなみにこれらはそれぞれ標準ライブラリのList.mem
またはList.memq
、List.find_opt
、String.concat
の型だ)。
とはいうもののこれにも限界があり、同じ型の引数を複数取るような関数になると難易度がはね上がる。例えばstring -> int -> bytes -> int -> int -> unit
はどんな関数だろうか。戻り値がunit
なので副作用目的なのはわかるとして、OCamlのbytes
がmutableでstring
はimmutableなことを知っていれば、bytes
型の引数を書き換えることは想像できるが、あとのint
は長さや添字だろうかと想像はできても、実際何なのかはそのライブラリの設計方針を知らないと断言はできない。
答えを言ってしまうとこの関数は標準ライブラリのString.blit
で、String.blit src srcoff dst dstoff len
はsrc
のsrcoff
文字目からlen
文字をdst
のdstoff
文字目以降に書き込む。
OCamlの標準ライブラリは開始位置 + 長さ方式だということを覚えておけばこれも型から推測できるようにはなるだろう。とはいうものの、複数言語を渡り歩いているとそんなことは覚えていられない。
こんなときに救いになるのがラベル付き引数だ(長い前置きだった)。
ラベル付き引数の基本
ラベル付き引数は他の言語で名前付き引数と呼ばれているものとほとんど同じだ。1
OCamlのString
モジュールにはStringLabels
というラベル付き版の変種があり、StringLabels.blit
にはsrc:string -> src_pos:int -> dst:bytes -> dst_pos:int -> len:int -> unit
という型がついている。型に引数の名前が入っているのが特徴だ。
使うときには次のように ~src:値
のようにチルダ、ラベル名、コロンを前置して引数を渡す。
utop #
let bs = Bytes.make 8 '_' in
StringLabels.blit ~src:"abcd" ~src_pos:1 ~dst:bs ~dst_pos:0 ~len:2; bs
;;
- : bytes = Bytes.of_string "bc______"
名前で引数を識別するので引数の順番は自由に入れ換えられる。ふつうの関数と同じようにカリー化されているので、一部の引数だけ指定することもできる。
utop # StringLabels.blit ~src:"abcd" ~src_pos:1 ~len:2;;
- : dst:bytes -> dst_pos:int -> unit = <fun>
ラベルなし引数と混ぜることもできるし、それらとの間で順番を入れ換えて適用することもできる。ListLabels.map
は関数引数をf
というラベル付きで受け取る。関数引数が長い場合に後ろに持って行ったりできるのが便利だ。
utop # ListLabels.map;;
- : f:('a -> 'b) -> 'a list -> 'b list = <fun>
utop # ListLabels.map [1; 2; 3] ~f:(fun x -> x * x);;
- : int list = [1; 4; 9]
(最近はパイプライン演算子があるので [1; 2; 3] |> List.map (fun x -> x * x)
でもいいのだが)
また、ラベル名と引数の変数名が同じ場合は変数名を省略して~dst:dst
を~dst
と書くことができる。したがって、先程のStringLabels.blit
の例は下記のようにも書ける。
utop #
let dst = Bytes.make 8 '_' in
StringLabels.blit ~src:"abcd" ~src_pos:1 ~dst ~dst_pos:0 ~len:2; dst
;;
- : bytes = Bytes.of_string "bc______"
ラベル付き引数を取る関数を定義する
ラベル付き引数を取る関数を定義するには、単に引数名の前に~
を前置すればよい。
utop # let power ~base n = Float.pow base n;;
val power : base:float -> float -> float = <fun>
utop # power ~base:2.0 4.0;;
- : float = 16.
~ラベル名:引数名
の形式にすればラベル名と引数名を別にできるが、こちらはそれほど使用頻度は高くないだろう。
utop # let power ~base:b n = Float.pow b n;;
val power : base:float -> float -> float = <fun>
オプショナル引数
ラベル付き引数の変種としてオプショナル引数がある。ラベル付き引数でoption
型を取るのとほぼ同じだが、いくつか便利な機能がある。
まず、名前の通り引数を省略できる(オプショナル引数は~
の代わりに?
を使って定義する)。
utop # let f ?x y = (x, y);;
val f : ?x:'a -> 'b -> 'a option * 'b = <fun>
utop # f 42;;
- : 'a option * int = (None, 42)
適用時に ~
を使えば値は自動的に Some
で包まれる。
utop # f ~x:3.14 42;;
- : float option * int = (Some 3.14, 42)
option
型の値を陽に渡したい場合は適用時にも?
を使う。オプショナル引数を取る関数から別のオプショナル引数を取る関数に引数を受け渡す場合に便利だ。
utop # f ?x:(Some "foobar") 0;;
- : string option * int = (Some "foobar", 0)
また、デフォルト値を指定することもできる。下記のiota
関数は start
から始めて要素間の差分を step
とし、 n
要素のリストを作成する関数だが、 start
と step
は省略時のデフォルト値はそれぞれ0と1になる。関数内部ではoption
型のunwrapも自動的に行われていてどちらも素のint
型の値で扱える。
utop # let iota ?(start = 0) ?(step = 1) n = List.init n (fun i -> i * step + start);;
val iota : ?start:int -> ?step:int -> int -> int list = <fun>
utop # iota 10;;
- : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9]
utop # iota ~start:1 10;;
- : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]
utop # iota ~start:3 ~step:2 3;;
- : int list = [3; 5; 7]
オプショナル引数ももちろん引数の順番は自由に入れ換えられる。
utop # iota 3 ~start:5;;
- : int list = [5; 6; 7]
utop # iota ~step:2 3 ~start:(-1);;
- : int list = [-1; 1; 3]
ただし、引数を省略可能にするためには制限もある。オプショナル引数のあとにラベルなしの必須引数を1つ以上取らなければならない。先程のf
の定義で引数の順番を入れ換えると警告が出る。
utop # let f y ?x = (x, y);;
Line 1, characters 9-10:
Warning 16: this optional argument cannot be erased.
val f : 'a -> ?x:'b -> 'b option * 'a = <fun>
警告の内容通り、この f
の ?x
引数は省略できない。
utop # f 0;;
- : ?x:'_weak1 -> '_weak1 option * int = <fun>
utop # f 0 ?x:None;;
- : 'a option * int = (None, 0)
必須引数がひとつでもあるのであれば、定義時にそれが最後の引数になるように並べ替えればよい。そうでない場合は、unit
型の引数を追加しなければならない。
utop # let g ?x () = x;;
val g : ?x:'a -> unit -> 'a option = <fun>
utop # g ();;
- : 'a option = None
utop # g ~x:0;;
- : unit -> int option = <fun>
utop # g ~x:0 ();;
- : int option = Some 0
ラベル付き引数が定義時に後ろにあっても省略可能にならないのは罠だと思う。
utop # let h ?y ~x = (x, y);;
val h : ?y:'a -> x:'b -> 'b * 'a option = <fun>
utop # h ~x:42;;
- : ?y:'a -> int * 'a option = <fun>
utop # let h ?y ~x () = (x, y);;
val h : ?y:'a -> x:'b -> unit -> 'b * 'a option = <fun>
utop # h ~x:42 ();;
- : int * 'a option = (42, None)
他の言語機能と組み合わせる
ラベル付き引数は単体でも便利だが、他の言語機能と組み合わせるとさらに威力を発揮する。
オススメなのが、レコード型との組み合わせだ。レコード型には{ field = field }
を省略して{ field }
と書けるpunningという機能があるが、これとラベル付き引数を組み合わせると、いい感じに型のついたレコード生成関数が簡潔に書ける(ppx_fields_convを使うとこのような関数を自動生成できる)。
utop # type location = { start_line: int; start_col: int; end_line: int; end_col: int };;
type location = {
start_line : int;
start_col : int;
end_line : int;
end_col : int;
}
utop #
let make_location ~start_line ~start_col ~end_line ~end_col =
{ start_line; start_col; end_line; end_col }
;;
val make_location :
start_line:int -> start_col:int -> end_line:int -> end_col:int -> location =
<fun>
型が int -> int -> int -> int -> location
より説明的になりいい感じだ。
バリアント型も関数と同じく同じ型の引数が続くとつらくなりがちだが、今時はバリアント型の引数にインラインレコードが指定できるようになったのでレコード型のときと同じ手法を使っておくといいと思う。
utop # type shape =
| Circle of { radius: float }
| Rect of { width: float; height: float }
(* Rect of float * float だとどっちがwidthだっけとなる *)
let make_rect ~width ~height = Rect { width; height }
;;
type shape =
Circle of { radius : float; }
| Rect of { width : float; height : float; }
val make_rect : width:float -> height:float -> shape = <fun>
utop # make_rect ~width:4.0 ~height:3.0;;
- : shape = Rect {width = 4.; height = 3.}
まとめ
OCamlのラベル付き引数を紹介した。筆者はオプショナル引数のoption
がいい感じにwrap/unwrapされる仕様がお気に入りで、他の言語でも同様の仕組みがあればいいのにと日々思っている。
-
歴史的には2000年にリリースされたOCaml 3.00で入った機能で、けっこう古参の機能でもある(前身となるLabl Lightは1995年ごろからある。A Label-Selective Lambda-Calculus with Optional Arguments and its Compilation Methodも参照)。 ↩