LoginSignup
4
0

More than 1 year has passed since last update.

ラベル付き引数の紹介

Posted at

この記事はML Advent Calendar 2021 5日目の記事です。

OCamlのラベル付き引数を紹介します。

前置き

静的型付き言語の利点のひとつとして、型を見ればなんとなく処理の内容の想像がつくという点がある。

'a -> 'a list -> boolであればリストに値が含まれるか判定する関数だろうし、('a -> bool) -> 'a list -> 'a optionであればリストから述語を満たす値を見つける関数だろう。string -> string list -> stringだと探索関数っぽさもあるけれど、文字列のリストの結合関数かなという気がする(ちなみにこれらはそれぞれ標準ライブラリのList.memまたはList.memqList.find_optString.concatの型だ)。

とはいうもののこれにも限界があり、同じ型の引数を複数取るような関数になると難易度がはね上がる。例えばstring -> int -> bytes -> int -> int -> unitはどんな関数だろうか。戻り値がunitなので副作用目的なのはわかるとして、OCamlのbytesがmutableでstringはimmutableなことを知っていれば、bytes型の引数を書き換えることは想像できるが、あとのintは長さや添字だろうかと想像はできても、実際何なのかはそのライブラリの設計方針を知らないと断言はできない。

答えを言ってしまうとこの関数は標準ライブラリのString.blitで、String.blit src srcoff dst dstoff lensrcsrcoff文字目からlen文字をdstdstoff文字目以降に書き込む。

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 要素のリストを作成する関数だが、 startstep は省略時のデフォルト値はそれぞれ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される仕様がお気に入りで、他の言語でも同様の仕組みがあればいいのにと日々思っている。


  1. 歴史的には2000年にリリースされたOCaml 3.00で入った機能で、けっこう古参の機能でもある(前身となるLabl Lightは1995年ごろからある。A Label-Selective Lambda-Calculus with Optional Arguments and its Compilation Methodも参照)。 

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0