M.Hiroi's Home Page

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

高階関数


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

はじめに

Lisp や ML などの関数型言語は、関数を他のデータ型と同等に取り扱うことができます。つまり、関数を変数に代入したり、引数として渡すことができるのです。また、値として関数を返すこともできるので、関数を作る関数を定義することもできます。関数を引数にとる関数のことを「汎関数 (functional)」とか「高階関数 (higher order function)」と呼びます。

OCaml は高階関数を簡単に定義することができます。もちろん、値として関数を返すこともできるので、関数を作る関数を定義することも簡単です。実際、関数の操作は Common Lisp よりも柔軟で簡単です。

●関数を引数にとる関数

簡単な例として、整数 n から m までの和を求める関数 sum_of_integer を作ってみましょう。末尾再帰でプログラムすると、次のようになります。

リスト 1 : 整数の和

let rec sum_of_integer (n, m, a) =
  if n > m then a
  else sum_of_integer (n + 1, m, a + n)

実行例を示します。

# sum_of_intger (1, 10, 0);;
- : int = 55

プログラムは簡単なので説明は不要でしょう。それでは、整数の 2 乗の和と 3 乗の和を求めるプログラムはどうなるでしょうか。次のリストを見てください。

リスト 2 : 整数の 2 乗の和と 3 乗の和

(* 2 乗 *)
let square x = x * x

(* 3 乗 *)
let cube x = x * x * x

(* 2 乗の和 *)
let rec sum_of_square (n, m, a) =
  if n > m then a
  else sum_of_square (n + 1, m, a + square n)

(* 3 乗の和 *)
let rec sum_of_cube (n, m, a) =
  if n > m then a
  else sum_of_cube (n + 1, m, a + cube n)

実行例を示します。

# sum_of_square (1, 10, 0);;
- : int = 385
# sum_of_cube (1, 10, 0);;
- : int = 3025

累積変数 a に値を加算する処理で、関数 square を呼び出すと 2 乗の和を、関数 cube を呼び出すと 3 乗の和を求めることができます。関数 sum_of_square と sum_of_cube の違いはこれだけです。ここで、square や cube を引数として渡すことができれば、sum_of_square や sum_of_cube を一つの関数で済ますことができます。次のリストを見てください。

リスト 3 : 高階関数 sum_of

let rec sum_of (f, n, m, a) =
  if n > m then a
  else sum_of (f, n + 1, m, a + f n)

関数 sum_of の第 1 引数 f に関数を渡します。渡された関数は、今までと同じ方法で呼び出すことができます。square や cube のかわりに a + f n とすることで、関数 f に整数 n を適用した結果を a に加算することができます。

OCaml の場合、let で定義された関数は、関数型データとしてその名前の変数に格納されています。したがって、次に示すように関数 sum_of の第 1 引数に square や cube を指定するだけで、変数に格納されている関数を渡すことができます。

# sum_of (square, 1, 10, 0);;
- : int = 385
# sum_of (cube, 1, 10, 0);;
- : int = 3025

●多相性

ところで、sum_of_integer も sum_of を使って実現することができます。引数 f には自分自身を返す関数を渡せばよいのです。これを「恒等関数 (identity function)」といいます。次の例を見てください。

# let identity x = x;;
val identity : 'a -> 'a = <fun>
# identity 10;;
- : int = 10
# identity 1.5;;
- : flaot = 1.5
# identity "foo";;
- : string = "foo"
# identity (1, 2);;
- : int * int = (1, 2)

関数 identity は引数をそのまま返す関数です。'a は「型変数」といって、任意のデータ型を表します。identity の型は 'a -> 'a なので、任意の型のデータを受け取り、同じ型を返す関数であることがわかります。つまり、int, float, string や組といった OCaml のデータ型であれば何でも対応することができるのです。このような関数を「多相型関数 (polymorphic function)」といいます。

多相型関数のように、いろいろな型を取ることができる性質のことを「多相性 (polymmprphism)」といいます。多相性は ML (OCaml, SML/NJ など) の特徴のひとつです。たとえば、要素が 2 つの組から最初の要素を取り出す関数 first を作ってみましょう。

# let first (x, y) = x;;
val first : ('a * 'b) -> 'a = <fun>

first は組の要素が何であっても対応することができます。実行例を示しましょう。

# first (1, 2);;
- : int = 1
# first ("foo", 10);;
- : string = "foo"

なお、OCaml には first と同じ動作を行う関数 fst が定義されています。

sum_of_integer は sum_of と identity を使って次のように実現できます。

# sum_of (identity, 1, 10, 0);;
- : int = 55

OCaml は型チェックを厳密に行うプログラミング言語ですが、型推論と多相性により柔軟なプログラミングが可能になっています。

●匿名関数とカリー化

高階関数を使うようになると、数を 2 乗する square のような小さな関数をいちいち定義するのが面倒になります。とくに、その高階関数でしか使わないのであれば、なおさらそう思うでしょう。ML には Lisp のラムダ式 (lambda) のような「匿名関数 (anonymous function)」という名前のない関数が用意されています。

OCaml の場合、匿名関数は次のように定義します。

fun 引数 -> 式

let による関数定義は、匿名関数を用いて次のように表すことができます。

let func = fun 引数 -> 式

匿名関数は関数型のデータを生成して返します。そして、let は変数 func をその値に束縛するだけなのです。また、OCaml は匿名関数をそのまま実行することができますし、関数の引数に匿名関数を渡すこともできます。

# (fun x -> x * x) 5;;
- : int = 25
# (fun (x, y) -> x + y) (1, 2);;
- : int = 3
# sum_of ((fun x -> x * x), 1, 10, 0);;
- : int = 385

ところで、今までは組を使って複数の引数を受け取る関数を実現しましたが、ML にはもう一つ方法があります。関数型言語は関数をデータ型の一つとして扱うことができるので、関数の返り値として関数を返すことができます。この「関数を返す関数」を使うと、関数の引数が一つでも複数の引数を処理することができます。このような関数を「カリー化関数 (curried function)」といいます。

たとえば、fun (x, y) -> x + y の場合、引数 x を受け取ると「引数 y を受け取って x + y を計算する関数」を返し、その関数に引数 y を渡せば x + y を計算することができます。OCaml では、次のように定義することができます。

# fun x -> fun y -> x + y;;
- : int -> int -> int = <fun>

関数の型が少し複雑になりました。-> は右結合なので、int -> int -> int は int -> (int -> int) となります。これは引数 int を受け取り、int -> int という型の関数を返すことを表しています。これで引数を一つだけ与えれば、関数を返すことになります。もちろん、引数を 2 つ与えれば、それらを加算した結果を返します。つまり、最初の引数を受け取って関数を生成し、その関数を 2 番目の引数に適用する、という動作になります。

それでは実際に試してみましょう。

# (fun x -> fun y -> x + y) 1;;
- : int -> int = <fun>
# (fun x -> fun y -> x + y) 1 2;;
- : int = 3

引数を一つだけ渡すと「引数 y を受け取って x + y を計算する関数」を返します。引数を 2 つ渡すと、それを足し算した値を返します。カリー化関数の場合、引数は空白で区切ることに注意してください。

fun を入れ子にするのは面倒なので、OCaml は次のようにカリー化関数を定義することができます。

fun 引数1 引数2 ... 引数n -> 式

また、let 式でも同様にカリー化関数を定義することができます。

let func 引数1 引数2 ... 引数n = 式

関数をカリー化する場合、引数をカッコで囲わず、カンマでも区切りません。たとえば、関数 sum_of をカリー化すると次のようになります。

リスト 4 : 関数 sum_of のカリー化

let rec sum_of f n m a =
  if n > m then a
  else sum_of f (n + 1) m (a + f n)

そして、この sum_of を使うと、sum_of_integer, sum_of_square, sum_of_cube を簡単に定義することができます。

# let sum_of_integer = sum_of (fun x -> x);;
val sum_of_integer : int -> int -> int -> int = <fun>
# let sum_of_square = sum_of (fun x -> x * x);;
val sum_of_square : int -> int -> int -> int = <fun>
# let sum_of_cube = sum_of (fun x -> x * x * x);;
val sum_of_cube : int -> int -> int -> int = <fun>
# sum_of_integer 1 10 0;;
- : int = 55
# sum_of_square 1 10 0;;
- : int = 385
# sum_of_cube 1 10 0;;
- : int = 3025

このように、カリー化された関数の一部の引数に値を与えて、残りの引数を受け取る関数を生成することを「部分適用 (partial application)」といいます。カリー化関数は部分適用が簡単にできるのでとても便利です。OCaml の場合、カリー化関数はよく使われる方法です。

●問題

次の関数を定義してください。

  1. 2 点間 (二次元) の距離を求める distance をカリー化関数で定義
  2. 2 点間 (三次元) の距離を求める distance3d をカリー化関数で定義
  3. 原点 (0.0, 0.0) との距離 (ベクトルの大きさ) を求める関数 norm
  4. 原点 (0.0, 0.0, 0.0) との距離を求める関数 norm3d














●解答

リスト : 2 点間の距離 (distance.ml)

let distance (x1, y1) (x2, y2) =
  let dx = x1 -. x2 in
  let dy = y1 -. y2 in
  sqrt (dx *. dx +. dy *. dy)

let distance3d (x1, y1, z1) (x2, y2, z2) =
  let dx = x1 -. x2 in
  let dy = y1 -. y2 in
  let dz = z1 -. z2 in
  sqrt (dx *. dx +. dy *. dy +. dz *. dz)

let norm = distance (0.0, 0.0)

let norm3d = distance3d (0.0, 0.0, 0.0)
# #use "distance.ml";;
val distance : float * float -> float * float -> float = <fun>
val distance3d : float * float * float -> float * float * float -> float = <fun>
val norm : float * float -> float = <fun>
val norm3d : float * float * float -> float = <fun>

# distance (0.0,0.0) (1.0,1.0);;
- : float = 1.41421356237309515
# norm (1.0,1.0);;
- : float = 1.41421356237309515
# distance3d (0.0,0.0,0.0) (1.0,1.0,1.0);;
- : float = 1.73205080756887719
# norm3d (1.0,1.0,1.0);;
- : float = 1.73205080756887719

初版 2008 年 6 月 14 日
改訂 2020 年 6 月 21 日