M.Hiroi's Home Page

お気楽 F# プログラミング超入門

高階関数


Copyright (C) 2022 Makoto Hiroi
All rights reserved.

はじめに

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

F# は高階関数を簡単に定義することができます。もちろん、値として関数を返すこともできるので、関数を作る関数を定義することも簡単です。実際、関数の操作は 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)

実行例を示します。

> let rec sum_of_integer (n, m, a) =
-   if n > m then a
-   else sum_of_integer (n + 1, m, a + n);;
val sum_of_integer: n: int * m: int * a: int -> int

> sum_of_integer (1, 10, 0);;
val it: 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)

実行例を示します。

> let rec sum_of_square (n, m, a) =
-   if n > m then a
-   else sum_of_square (n + 1, m, a + square n);;
val sum_of_square: n: int * m: int * a: int -> int

> let rec sum_of_cube (n, m, a) =
-   if n > m then a
-   else sum_of_cube (n + 1, m, a + cube n);;
val sum_of_cube: n: int * m: int * a: int -> int

> sum_of_square (1, 10, 0);;
val it: int = 385

> sum_of_cube (1, 10, 0);;
val it: 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 に加算することができます。

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

> let rec sum_of (f, n, m, a) =
-   if n > m then a
-   else sum_of (f, n + 1, m, a + f n);;
val sum_of: f: (int -> int) * n: int * m: int * a: int -> int

> sum_of (square, 1, 10, 0);;
val it: int = 385

> sum_of (cube, 1, 10, 0);;
val it: int = 3025

●多相性

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

> let identity x = x;;
val identity: x: 'a -> 'a

> identity 10;;
val it: int = 10

> identity 1.5;;
val it: float = 1.5

> identity "foo";;
val it: string = "foo"

> identity (1, 2);;
val it: int * int = (1, 2)

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

多相型関数のように、いろいろな型を取ることができる性質のことを「多相性 (polymmprphism)」といいます。多相性は ML (OCaml, SML/NJ など) やそれをベースとした F# の特徴のひとつです。

たとえば、要素が 2 つのタプルから最初の要素を取り出す関数 first を作ってみましょう。

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

> first;;
val it: ('a * 'b -> 'a)

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

> first (1, 2);;
val it: int = 1

> first ("foo", 10);;
val it: string = "foo"

なお、F# には first と同じ動作を行う関数 fst や、2 番目の要素を取り出す関数 snd が定義されています。

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

> sum_of (identity, 1, 10, 0);;
val it: int = 55

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

●ラムダ式とカリー化

高階関数を使うようになると、数を 2 乗する square のような小さな関数をいちいち定義するのが面倒になります。とくに、その高階関数でしか使わないのであれば、なおさらそう思うでしょう。F# の場合、「ラムダ式」という名前のない関数を定義することができます。基本的には Lisp / Scheme のラムダ式と同じです。ML 系の言語では「匿名関数 (anonymous function)」といいます。

F# の場合、ラムダ式は次のように定義します。

fun 引数 -> 式

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

let func = fun 引数 -> 式

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

> (fun x -> x * x) 5;;
val it: int = 25

> (fun (x, y) -> x + y) (1, 2);;
val it: int = 3

> sum_of ((fun x -> x * x), 1, 10, 0);;
val it: int = 385

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

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

> fun x -> fun y -> x + y;;
val it: x: int -> y: int -> int

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

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

> (fun x -> fun y -> x + y) 1;;
val it: (int -> int) = <fun:it@84-6>

> (fun x -> fun y -> x + y) 1 2;;
val it: int = 3

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

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

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 rec sum_of f n m a =
-   if n > m then a
-   else sum_of f (n + 1) m (a + f n);;
val sum_of: f: (int -> int) -> n: int -> m: int -> a: int -> int

> let sum_of_integer = sum_of (fun x -> x);;
val sum_of_integer: (int -> int -> int -> int)

> let sum_of_square = sum_of (fun x -> x * x);;
val sum_of_square: (int -> int -> int -> int)

> let sum_of_cube = sum_of (fun x -> x * x * x);;
val sum_of_cube: (int -> int -> int -> int)

> sum_of_integer 1 10 0;;
val it: int = 55

> sum_of_square 1 10 0;;
val it: int = 385

> sum_of_cube 1 10 0;;
val it: int = 3025

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

●問題

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

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






















●解答

> let distance (x1, y1) (x2, y2) : float =
-   let dx = x1 - x2 in
-   let dy = y1 - y2 in
-   sqrt (dx * dx + dy * dy);;
val distance: x1: float * y1: float -> x2: float * y2: float -> float

> distance (0.0,0.0) (1.0,1.0);;
val it: float = 1.414213562

> let distance3d (x1, y1, z1) (x2, y2, z2) : float =
-   let dx = x1 - x2 in
-   let dy = y1 - y2 in
-   let dz = z1 - z2 in
-   sqrt (dx * dx + dy * dy + dz * dz);;
val distance3d:
  x1: float * y1: float * z1: float -> x2: float * y2: float * z2: float
    -> float

> distance3d (0.0,0.0,0.0) (1.0,1.0,1.0);;
val it: float = 1.732050808

> let norm = distance (0.0, 0.0);;
val norm: (float * float -> float)

> norm (1.0,1.0);;
val it: float = 1.414213562

> let norm3d = distance3d (0.0, 0.0, 0.0);;
val norm3d: (float * float * float -> float)

> norm3d (1.0,1.0,1.0);;
val it: float = 1.732050808

初版 2022 年 3 月 6 日