ラベル付き引数の紹介

この記事は[ML Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ml) 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]

[^1]: 歴史的には2000年にリリースされたOCaml 3.00で入った機能で、けっこう古参の機能でもある（前身となるLabl Lightは1995年ごろからある。[A Label-Selective Lambda-Calculus with Optional Arguments and its Compilation Method](https://www.math.nagoya-u.ac.jp/~garrigue/papers/rims-1041.pdf)も参照）。

OCamlの`String`モジュールには`StringLabels`というラベル付き版の変種があり、`StringLabels.blit`には`src:string -> src_pos:int -> dst:bytes -> dst_pos:int -> len:int -> unit`という型がついている。型に引数の名前が入っているのが特徴だ。

使うときには次のように `~src:値` のようにチルダ、ラベル名、コロンを前置して引数を渡す。

```ocaml
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______"
```

名前で引数を識別するので引数の順番は自由に入れ換えられる。ふつうの関数と同じようにカリー化されているので、一部の引数だけ指定することもできる。

```ocaml
utop # StringLabels.blit ~src:"abcd" ~src_pos:1 ~len:2;;
- : dst:bytes -> dst_pos:int -> unit = <fun>
```

ラベルなし引数と混ぜることもできるし、それらとの間で順番を入れ換えて適用することもできる。`ListLabels.map`は関数引数を`f`というラベル付きで受け取る。関数引数が長い場合に後ろに持って行ったりできるのが便利だ。

```ocaml
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`の例は下記のようにも書ける。

```ocaml
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______"
```

# ラベル付き引数を取る関数を定義する

ラベル付き引数を取る関数を定義するには、単に引数名の前に`~`を前置すればよい。

```ocaml
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.
```

`~ラベル名:引数名` の形式にすればラベル名と引数名を別にできるが、こちらはそれほど使用頻度は高くないだろう。

```ocaml
utop # let power ~base:b n = Float.pow b n;;
val power : base:float -> float -> float = <fun>
```

# オプショナル引数

ラベル付き引数の変種としてオプショナル引数がある。ラベル付き引数で`option`型を取るのとほぼ同じだが、いくつか便利な機能がある。

まず、名前の通り引数を省略できる（オプショナル引数は`~`の代わりに`?`を使って定義する）。

```ocaml
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` で包まれる。

```ocaml
utop # f ~x:3.14 42;;
- : float option * int = (Some 3.14, 42)
```

`option`型の値を陽に渡したい場合は適用時にも`?`を使う。オプショナル引数を取る関数から別のオプショナル引数を取る関数に引数を受け渡す場合に便利だ。

```ocaml
utop # f ?x:(Some "foobar") 0;;
- : string option * int = (Some "foobar", 0)
```

また、デフォルト値を指定することもできる。下記の`iota`関数は `start` から始めて要素間の差分を `step` とし、 `n` 要素のリストを作成する関数だが、 `start` と `step` は省略時のデフォルト値はそれぞれ0と1になる。関数内部では`option`型のunwrapも自動的に行われていてどちらも素の`int`型の値で扱える。

```ocaml
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]
```

オプショナル引数ももちろん引数の順番は自由に入れ換えられる。

```ocaml
utop # iota 3 ~start:5;;
- : int list = [5; 6; 7]

utop # iota ~step:2 3 ~start:(-1);;
- : int list = [-1; 1; 3]
```

ただし、引数を省略可能にするためには制限もある。オプショナル引数のあとにラベルなしの必須引数を1つ以上取らなければならない。先程の`f`の定義で引数の順番を入れ換えると警告が出る。

```ocaml
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` 引数は省略できない。

```ocaml
utop # f 0;;
- : ?x:'_weak1 -> '_weak1 option * int = <fun>

utop # f 0 ?x:None;;
- : 'a option * int = (None, 0)
```

必須引数がひとつでもあるのであれば、定義時にそれが最後の引数になるように並べ替えればよい。そうでない場合は、`unit`型の引数を追加しなければならない。

```ocaml
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
```

ラベル付き引数が定義時に後ろにあっても省略可能にならないのは罠だと思う。

```ocaml
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](https://opam.ocaml.org/packages/ppx_fields_conv/)を使うとこのような関数を自動生成できる）。

```ocaml
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` より説明的になりいい感じだ。

バリアント型も関数と同じく同じ型の引数が続くとつらくなりがちだが、今時はバリアント型の引数にインラインレコードが指定できるようになったのでレコード型のときと同じ手法を使っておくといいと思う。

```ocaml
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>
```

```ocaml
utop # make_rect ~width:4.0 ~height:3.0;;
- : shape = Rect {width = 4.; height = 3.}
```

# まとめ

OCamlのラベル付き引数を紹介した。筆者はオプショナル引数の`option`がいい感じにwrap/unwrapされる仕様がお気に入りで、他の言語でも同様の仕組みがあればいいのにと日々思っている。