M.Hiroi's Home Page

お気楽 OCaml プログラミング入門

ラベル付き引数とオプション引数


Copyright (C) 2008-2020 Makoto Hiroi
All rights reserved.

はじめに

OCaml は関数の引数に「ラベル (label)」を付けることができます。これをラベル付き引数といいます。引数の個数が多い関数は、その順番を覚えるのが大変です。ラベル付き引数はラベル名で引数を指定できるので、引数の順番を覚える必要はありません。つまり、引数の順番を変えることができるのです。これは、カリー化関数で部分適用を行うときにとても役に立ちます。

OCaml の標準ライブラリには Labels で終わるモジュール、たとえば、ArrayLabels や ListLabels などがあります。これらのモジュールを使うと、Array や List の関数をラベル付き引数で使用することができます。

●ラベル付き引数

ラベル付き引数は次のように指定します。

~ラベル名:パターン

実引数は ~ラベル名:実引数 と指定します。標準モジュールで使用されている主なラベル名を表に示します。

表 1 : 主なラベル名
ラベル名意味
f関数
pos位置
len長さ
bufバッファ
src操作対象元
dst操作対象先
init初期値
cmp比較関数
modeモード

簡単な例題としてマップ関数をラベル付き引数でプログラムしてみましょう。次のリストを見てください。

リスト 1 : ラベル付き引数 (1)

let rec map ~f:func = function
  [] -> []
| (x::xs) -> func x :: map ~f:func xs

ラベル名とパターンが一致する場合、パターンを省略することができます。プログラムは次のようになります。

リスト 2 : ラベル付き引数 (2)

let rec map ~f = function
  [] -> []
| (x::xs) -> f x :: map ~f xs

簡単な実行例を示します。

# map ~f:(fun x -> x * x) [1; 2; 3; 4; 5];;
- : int list = [1; 4; 9; 16 ;25]
# map [1; 2; 3; 4; 5] ~f:(fun x -> x * x);;
- : int list = [1; 4; 9; 16 ;25]

もう一つ簡単な例を示しましょう。畳み込み関数 fold_left と fold_right をラベル付き引数でプログラムします。次のリストを見てください。

リスト 3 : 畳み込み関数

let rec fold_right ~f ls ~init =
  match ls with
    [] -> init
  | x::xs -> f x (fold_right ~f xs ~init)

let rec fold_left ~f ls ~init =
  match ls with
    [] -> init
  | x::xs -> fold_left ~f xs ~init:(f init x)

これらの関数を使うと、length と reverse は次のように定義することができます。

# let length ls = fold_right ~f:(fun x y -> y + 1) ~init:0 ls
val length : 'a list -> int = <fun>
# let reverse ls = fold_left ~f:(fun x y -> y :: x) ~init:[] ls
val reverse : 'a list -> 'a list = <fun>

●引数の順番

引数の順番を変更できるのはラベル付き引数だけで、通常の引数は順番を変更することはできません。次の例を見てください。

リスト 4 : リストの生成 (1)

let rec tabulate ~f s e =
  if s > e then []
  else f s :: tabulate ~f (s + 1) e

関数 tabulate は整数 s から e までの値を関数 f に適用し、その結果をリストに格納して返します。関数 f はラベルを指定することで引数のどの位置にでも指定することができますが、引数 s と e の順番は変えることができません。ラベルなしの最初の実引数が s になり、次の実引数が e になります。

簡単な実行例を示しましょう。

# tabulate ~f:(fun x -> x * x) 1 5;;
- : int list = [1; 4; 9; 16; 25]
# tabulate 1 ~f:(fun x -> x * x) 5;;
- : int list = [1; 4; 9; 16; 25]
# tabulate 1 5 ~f:(fun x -> x * x);;
- : int list = [1; 4; 9; 16; 25]

なお、引数の順番が関数定義と同じで、すべての引数が与えられている場合に限り、ラベルを省略することができます。次の例を見てください

# tabulate (fun x -> x * x) 1 5;;
- : int list = [1; 4; 9; 16; 25]

ただし、関数によってはラベルを省略するとエラーになる場合があります。たとえば、fold_left や fold_right がそうです。次の例を見てください。

# fold_left (fun x y -> x + y) 0 [1;2;3;4;5];;
Characters 10-28:
  fold_left (fun x y -> x + y) 0 [1;2;3;4;5];;
            ^^^^^^^^^^^^^^^^^^
This expression should not be a function, the expected type is
'a list

これは、関数の返り値のデータ型によっては、すべての引数が与えられていないと OCaml が判断するため、最初の実引数 (関数) をラベルなしの引数 (リスト) に割り当てようとするために発生するようです。詳しいことは参考文献『プログラミング in OCaml』をお読みください。

●オプション引数

OCaml のラベル付き引数は、デフォルトの値を設定することができます。これを「オプション引数」といいます。関数を呼び出すとき、オプション引数は省略することができます。その場合、オプション引数の値はデフォルトの値が使用されます。

オプション引数は次のように指定します。

?ラベル名:(パターン:型=式)

:型 の部分は省略することができます。また、ラベル付き引数と同様に、ラベル名とパターンが同じ場合は、次のように指定することができます。

?(パターン=式)

実引数の指定方法はラベル付き引数と同じです。簡単な例を示しましょう。次のリストを見てください。

リスト 5 : リストの生成 (2)

let rec iota ?(step=1) s e =
  if s > e then []
  else s :: iota ~step (s + step) e

関数 iota は整数 s から e までの値を step 間隔でリストに格納して返します。step はオプション引数として宣言しているので、iota を呼び出すとき step の値を省略することができます。その場合、step の値は 1 になります。

実際に iota を定義すると、関数の型は次のように表示されます。

val iota : ?step:int -> int -> int -> int list = <fun>

このように、step の前にオプション引数であることを示す ? が付きます。

それでは簡単な実行例を示しましょう。

# iota 1 5;;
- : int list = [1; 2; 3; 4; 5]
# iota 1 10 ~step:2;;
- : int list = [1; 3; 5; 7; 9]

iota 1 5 は step が省略されているので返り値は [1; 2; 3; 4; 5] になります。次の例では step が 2 なので、数値の間隔は 2 になります。オプション引数はラベル付き引数と同様に順番を変えることができます。

ただし、最後の引数をオプション引数にすると、オプション引数を省略することができなくなります。たとえば、次のように iota 定義すると、警告が表示されます。

# let rec iota s e ?(step=1) =
  if s > e then []
  else iota (s + step) e ~step;;
Characters 24-25:
Warning X: this optional argument cannot be erased.
  let rec iota s e ?(step=1) =
                          ^
val iota : int -> int -> ?step:int -> 'a list = <fun>
# iota 1 10;;
- : ?step:int -> 'a list = <fun>

このように、関数を定義することはできますが、オプション引数 step を省略することはできません。オプション引数は他の引数よりも前に定義してください。もしも、すべての引数をオプション引数にする場合は、最後に unit を渡すようにしてください。簡単な例を示しましょう。

# let foo ?(a=1) ?(b=2) () = Printf.printf "(%d,%d)\n" a b;;
val foo : ?a;int -> ?b;int -> unit -> unit = <fun>
# foo ();;
(1,2)
- : unit = ()

オプション引数は多数の引数を必要とする関数を定義するときに使うと便利です。たとえば、GUI ライブラリ LablTk ではオプション引数を多用しています。

●問題

オプション引数を使って次に示す関数を末尾再帰で定義してください。

  1. 階乗を求める関数 fact n
  2. フィボナッチ数を求める関数 fibo n
  3. 要素 x を n 個持つリストを生成する関数 make_list x n
  4. リスト xs を反転する関数 reverse xs
  5. 述語 pred が真を返す要素を数える関数 count_if pred xs












●解答

# let rec fact ?(a=1) n =
  if n = 0 then a
  else fact ~a:(a * n) (n - 1);;
val fact : ?a:int -> int -> int = <fun>
# fact 10;;
- : int = 3628800
# fact 15;;
- : int = 1307674368000
# fact 20;;
- : int = 2432902008176640000

# let rec fibo ?(a=0) ?(b=1) n =
  if n = 0 then a
  else fibo ~a:b ~b:(a + b) (n - 1);;
val fibo : ?a:int -> ?b:int -> int -> int = <fun>
# fibo 9;;
- : int = 34
# fibo 10;;
- : int = 55
# fibo 11;;
- : int = 89

# let rec make_list ?(a=[]) x n =
  if n = 0 then a
  else make_list ~a:(x::a) x (n - 1);;
val make_list : ?a:'a list -> 'a -> int -> 'a list = <fun>
# make_list 1 10;;
- : int list = [1; 1; 1; 1; 1; 1; 1; 1; 1; 1]
# make_list 'a' 10;;
- : char list = ['a'; 'a'; 'a'; 'a'; 'a'; 'a'; 'a'; 'a'; 'a'; 'a']

# let rec reverse ?(a=[]) = function
  [] -> a
  | x::xs -> reverse ~a:(x::a) xs;;
val reverse : ?a:'a list -> 'a list -> 'a list = <fun>
# reverse [1;2;3;4;5];;
- : int list = [5; 4; 3; 2; 1]

# let rec count_if ?(c=0) pred = function
  [] -> c
  | x::xs -> count_if ~c:(if pred x then c + 1 else c) pred xs;;
val count_if : ?c:int -> ('a -> bool) -> 'a list -> int = <fun>
# count_if (fun x -> x mod 2 = 0) [1;2;3;4;5];;
- : int = 2
# count_if (fun x -> x mod 2 <> 0) [1;2;3;4;5];;
- : int = 3

初版 2008 年 8 月 17 日
改訂 2020 年 7 月 26 日