M.Hiroi's Home Page
http://www.geocities.jp/m_hiroi/

Functional Programming

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

[ PrevPage | OCaml | NextPage ]

データ型の定義

近代的なプログラミング言語は、ユーザーが既存のデータ型を組み合わせて、新しいデータ型を定義する機能を備えています。OCaml の場合、組を使って新しい型を表すことができますが、このほかに「ヴァリアント (variant) 」と「レコード (record) 」というデータ構造を定義する方法があります。

●ヴァリアントの定義

最初にヴァリアントから説明します。ヴァリアントはC言語の共用体とよく似ているデータ構造です。OCaml の場合、新しいデータ型を定義するときは type 宣言を使います。ヴァリアントを定義する構文を次に示します。

type (型変数, ... 型変数) 型の名前 =
  データ構成子A of 型式A
| データ構成子B of 型式B
   ...
| データ構成子N of 型式N

「型式 (type expression) 」とはデータ型を表す式のことで、基本的なデータ型である int, float, string, bool などのほかに、関数型や組を表す積型、list と組み合わせてできる int list や string list などがあります。データ構成子 (コンストラクタ) とは型式のデータを表す名前です。OCaml の場合、コンストラクタ名は英大文字から始めます。コンストラクタはデータを生成するときやパターンマッチングのときに使用します。

なお、異なるバリアント型で同名のコンストラクタを定義すると、後から定義したコンストラクタが有効になり、前に定義したコンストラクタは隠蔽されます。たとえば、ヴァリアント foo でコンストラクタ Baz を定義します。その後でバリアント bar でコンストラクタ Baz を定義すると、Baz で生成したデータ型は bar になります。Baz で foo のデータ型は生成できません。ご注意ください。

●ヴァリアントの生成

もっとも簡単なヴァリアントは、C言語などの列挙型 (enum) のように使う方法です。次の例を見てください。

# type fruit = Apple | Orange | Grape;;
type fruit = Apple | Orange | Grape
# Apple;;
- : fruit = Apple
# Grape;;
- : fruit = Grape
# Orange;;
- : fruit = Orange

fruit というデータ型は Apple, Orange, Grape から構成されます。このあと、Apple, Orange, Grape を OCaml のプログラムで使うことができます。次のように fruit を組やリストに入れることもできます。

# let a = [Apple; Orange; Grape];;
val a : fruit list = [Apple; Orange; Grape]
# let b = (Apple, Orange);;
val b : fruit * fruit = (Apple, Orange)

また、ヴァリアントはC言語の共用体とよく似た働きをします。たとえば、number というデータ型を定義してみましょう。次の例を見てください。

# type number = Int of int | Float of float;;
type number = Int of int | Float of float
# Int 10;;
- : number = Int 10
# Float 1.1;;
- : number = Float 1.1
# [Int 1; Float 1.5; Int 2; Float 2.5];;
- : number list = [Int 1; Float 1.5; Int 2; Float 2.5]

number は数を表すデータ型で、データは整数 (int) または実数 (float) です。Int of int によりコンストラクタ Int の型は int と定義され、Float of float でコンストラクタ Float の型は float と定義されます。

データ型 number の生成はコンストラクタを使います。Int 10 または Float 1.1 で number 型のデータが生成されます。Int (10) や Float (1.1) のようにカッコで囲んでもかまいません。最後の例のように、number 型のデータをリストに格納することができます。このように新しいデータ型 number を定義することで、整数と実数をいっしょにリストに格納することができます。

●パターンマッチング

コンストラクタは「パターン」として使うことができます。たとえば、number list の整数と実数の合計値を別々に求める関数 number_sum を作ってみましょう。次のリストを見てください。

リスト 1 : 整数と実数の合計値を別々に求める

let number_sum xs =
  let rec sum xs a b = 
    match xs with
      [] -> (a, b)
    | Int y :: ys -> sum xs (a + y) b
    | Float y :: ys -> sum xs a (b +. y)
  in
    sum xs 0 0.0

number_sum は整数と実数の合計を求め、組 (int * float) にして返します。実際の処理は局所関数 sum で行います。sum の第 2, 3 引数を累積変数として使い、第 2 引数に整数の合計値、第 3 引数に実数の合計値を求めます。Int y :: ys と Float y :: ys がパターンです。リストの先頭の要素が Int の場合、2 番目の定義とマッチングして、y の値は整数になります。要素が Float の場合は 3 番目の定義とマッチングして、y の値は実数になります。

それでは実行例を示します。

val number_sum = number list -> int * float = <fun>
# number_sum [Int 10; Int 11; Real 2.5; Int 12; Real 3.1; Real 4.2];;
- : int * float = (33, 9.8)

●option 型

型変数を使うと 'a list のような多相的なデータ型を作ることができます。簡単な例として、OCaml に定義されている option 型をみてみましょう。option の定義を示します。

type 'a option = None | Some of 'a

型変数は type と名前の間に指定します。複数の型変数を使う場合は ('a, 'b, ... 'n) のように、型変数をカッコで囲んでカンマで区切ります。option はデータの有無を表す型です。データが無い場合は None を、データが有る場合は Some を使います。Some の型式は 'a なので、どのデータ型でも option に格納することができます。次の例を見てください。

# None;;
- : 'a option = None
# Some 10;;
- : int option = Some 10
# Some "foo";;
- : string option = Some "foo"

None だけではデータ型を定めることができないので、型は 'a option と表示されます。Some 10 の場合、与えられたデータ型が int なので int option になります。同様に、Some "foo" の型は string option になります。

たとえば、リストからデータを探索する処理を考えてみましょう。「リストの探索」で作成した関数 member は、データが見つかれば残りのリストを、見つからなければ空リストを返すように作りました。見つけたデータをそのまま返さないのは、データが見つからない場合の返り値である空リストとデータ型を一致させるためです。

OCaml はデータ型を厳密にチェックするプログラミング言語なので、異なるデータ型を返すことはできません。このような場合、役に立つのが option です。見つけたデータをそのまま返さずに、option に格納して返せばいいわけです。

簡単な例として、リストの中から述語を満たす要素を探す関数 find_if を作ってみましょう。OCaml には List.find という同じ機能を持つ関数が用意されていますが、私達でも簡単に作ることができます。ちなみに、関数名は Common Lisp から拝借しました。

リスト 2 : リストの探索

let rec find_if f = function
  [] -> None
| x :: xs -> if f x then Some x else find_if f xs

最初の節で、データが見つからない場合は None を返します。次の節で、述語 f x が真を返せば Some x を返します。そうでなければ、find_if を再帰呼び出しして次の要素を調べます。

それでは、簡単な実行例を示します。

val find_if : ('a -> bool) -> 'a list -> 'a option
# find_if (fun x -> x >= 10) [1; 2; 11; 12; 3; 4];;
- : int option = Some 11
# find_if (fun x -> x >= 10) [1; 2; 3; 4; 5; 6];;
- : int option = None

データが見つからない場合は None を返しますが、リストの要素が int なので None のデータ型も int option になります。なお、option の値はパターンマッチングを使って取り出すことができます。

●再帰的なデータ型

ヴァリアントは再帰的なデータ構造も定義することができます。たとえば、連結リスト (linkedlist) は次のように定義することができます。

type 'a linkedlist = Nil | Cell of 'a * 'a linkedlist

Nil が連結リストの終端を表し、Cell がコンスセルを表します。積型 'a * 'a linkedlist で、最初の要素が格納するデータになり、2 番目の要素が次の Cell を格納するデータになります。これで Ocaml のリストと同等の多相的なデータ型になります。次の例を見てください。

# type 'a linkedlist = Nil | Cell of 'a * 'a linkedlist;;
type 'a linkedlist = Nil | Cell of 'a * 'a linkedlist
# let a = Cell (1, Nil);;
val a : int linkedlist = Cell (1, Nil)
# let b = Cell (2, a);;
val b : int linkedlist = Cell (2, Cell (1, Nil))
# let c = Cell (10, Cell (11, Cell (12, Nil)));;
val c : int linkedlist = Cell (10, Cell (11, Cell (12, Nil)))

このように、Cell を使って linkedlist を組み立てることができます。ようするに、コンストラクタ Cell が list のコンス演算子と同じ働きをしているわけです。実際、Ocaml の list は次のように定義されています。

type 'a list = [] | :: of 'a * 'a list

これで多相的なリスト構造を実現できるのですから、OCaml の型システムは柔軟で強力な機能だと思います。

●連想リスト

それでは簡単な例題として、「連想リスト (association list : a-list) 」を作ってみましょう。前回簡単に説明しましたが、連想リストは組 (key, data) を要素としたリストです。OCaml には連想リストを操作する関数が List モジュールに用意されています。今回は OCaml の学習ということで、あえて「連想リスト」を表すデータ型とその操作関数を作成してみましょう。

最初にヴァリアントでデータ型を定義します。

リスト 3 : 連想リストの定義

type ('a, 'b) alist = ANil | ACell of 'a * 'b * ('a, 'b) alist

名前は alist にしたので、型は ('a, 'b) alist になります。'a がキーで 'b がデータに対応する型になります。ANil が空の連想リストを表します。ACell の内部では 'a と 'b を組 ('a * 'b) にする必要はないので、ACell の型式は 'a * 'b * ('a, 'b) list としました。

次に連想リストを生成する関数を作りましょう。関数名は Common Lisp から拝借しました。次のリストを見てください。

リスト 4 : 連想リストの生成

let acons x y ls = ACell (x, y, ls)

let rec pairlis xs ys =
  match (xs, ys) with
    (([], _) | (_, [])) -> ANil
  | (x1::xs1, y1::ys1) -> ACell (x1, y1, (pairlis xs1 ys1))

関数 acons はキー x とデータ y と連想リスト ls を受け取り、ls の先頭に x と y を追加します。関数 pairlis は 2 つのリストを受け取り、第 1 引数のリストの要素がキー、第 2 引数のリストの要素がデータとなる連想リストを生成します。引数のリストの長さが違う場合は短い方に合わせます。

最初の節がどちらかのリストが空になった場合です。複数のパターンを | でつなぐパターンを「or パターン」といいます。パターンのどれかがマッチングに成功すれば、その節を選択して評価します。最後の節で ACell を生成して xs の要素 x1 と ys の要素 y1 を格納します。

次はデータを探索する関数を作ります。関数 assoc は指定したキーと等しいデータを探します。関数 assoc_if は述語が真となるキーを探します。どちらの関数も、データを option に格納して返します。見つからない場合は None を返します。

リスト 5 : 連想リストの探索

let rec assoc key = function
  ANil -> None
| ACell (x, y, rest) -> if x = key then Some y else assoc key rest

let rec assoc_if f = function
  ANil -> None
| ACell (x, y, rest) -> if f x then Some y else assoc_if f rest

どちらの関数もパターンマッチングで ACell 内のデータを取り出しています。x がキーで、y がデータ、rest が次の ACell です。連想リストが空 (ANil) の場合は None を返します。assoc は引数 x とキー a を比較して、等しければ Some b を返します。assoc_if は f x の評価結果が真であれば Some b を返します。

それでは、簡単な実行例を示します。

val acons : 'a -> 'b -> ('a, 'b) alist -> ('a, 'b) alist = <fun>
val pairlis : 'a list -> 'b list -> ('a, 'b) alist = <fun>
val assoc : 'a -> ('a, 'b) alist -> 'b option = <fun>
val assoc_if : ('a -> bool) -> ('a, 'b) alist -> 'b option = <fun>
# let a = acons 1 10 ANil;;
val a : (int, int) alist = ACell (1, 10, ANil)
# let b = acons 2 12 a;;
val b : (int, int) alist = ACell (2, 12, ACell (1, 10, ANil))
# let c = pairlis( [1, 2, 3, 4, 5], [11, 12, 13, 14, 15] );;
val c : (int,int) alist = ... 省略 ...

# assoc 4 c:;
- : int option = Some 14
# assoc_if (fun x -> x mod 2 = 0) c;;
^ : int option = Some 12

正常に動作していますね。

●レコードの定義

次は「レコード (record) 」について説明します。レコードはC言語の構造体と同じようなデータ構造です。レコードは type 宣言で次のように定義します。

type (型引数, ...) 型名 = {
  フィールド名1 : 型1; フィールド名2 : 型2; ... ; フィールド名n : 型n
}

type 宣言の後ろに型名を指定し、その後ろに中カッコ { } で本体を定義します。要素は "フィールド名 : 型" の形式で指定してカンマで区切ります。なお、異なるレコード型で同じフィールド名を使用することはできません。フィールド名が重複している場合、あとから定義したレコードが有効になり、その前に定義したレコードは使うことができなくなります。ご注意ください。

●レコードの生成

レコードは次の形式で生成します。

{ フィールド名1 = 式1; フィールド名2 = 式2; ... ; フィールド名n = 式n }

式の評価結果がフィールドの値になります。簡単な例を示しましょう。

# type foo = {bar : int; baz : float};;
type foo = { bar : int; baz : float; }
# let a = {bar = 10; baz = 1.23};;
val a : foo = {bar = 10; baz = 1.23}
# a.bar;;
- : int = 10
# a.baz;;
- : float = 1.23

フィールドの値はパターンマッチングで取り出すこともできますが、"レコード + ドット ( . ) + フィールド名" の形式で取り出すこともできます。

OCaml はフィールドの値を変更した新しいレコードを生成することができます。次の例を見てください。

# let b = {a with baz = 123.4};;
val b : foo = {bar = 10; baz = 123.4}
# a;;
val a : foo = {bar = 10; baz = 1.23}

{式0 with フィールド名 = 式; ... } という形式で、式0 で得られるレコードのフィールドの値を、指定した式の評価値に変更した新しいレコードを生成します。なお、元のレコードの値を書き換えることはありません。

●レコードのパターンマッチング

もちろん、レコードでもパターンマッチングを使うことができます。次の例を見てください。

# let {bar = x; baz = y} = a;;
val x : int = 10
val y : float = 1.23
# let {baz = z} = a;;
val z = 1.23

"フィールド = パターン" とするとフィールドの値とパターンを照合します。{bar = x; baz = y} は変数 x とフィールド bar の値、変数 y とフィールド bar の値がマッチングして、x = 10, y = 1.23 になります。また、レコードのパターンマッチングは、必要なフィールドだけを指定することができます。フィールド名からレコードの型が分かるので、すべてのフィールドを指定する必要はありません。

●多相的なレコード

レコードは型変数を使って多相的なデータ構造を定義することができます。たとえば、キーとデータの組を表すレコードは次のように定義することができます。

# type ('a, 'b) pair = {key : 'a; data : 'b};;
type ('a, 'b) pair = { key : 'a; data : 'b; }

キー (key) の型を 'a で、データ (data) の型を 'b で表しています。

簡単な例題として、pair を使って連想リストを作ってみましょう。レコードはヴァリアントと組み合わせることができます。次のリストを見てください。

リスト 6 : 連想リストの定義

type ('a, 'b) alist = ANil | ACell of ('a, 'b) pair * ('a, 'b) alist

ACell の型は ('a, 'b) pair * ('a, 'b) alist になります。

連想リストを生成する関数 acons と pairlis は次のようになります。

リスト 7 : 連想リストの生成

let acons x y xs = ACell ({key = x; data = y}, xs)

let rec pairlis xs ys =
  match (xs, ys) with
    (([], _) | (_, [])) -> ANil
  | (x1::xs1, y1::ys1) -> ACell ({key = x1; data = y1}, (pairlis xs1 ys1))

どちらの関数もキーとデータをレコードに格納するだけです。

データを探索する関数 assoc と assoc_if は次のようになります。

リスト 8 : 連想リストの探索

let rec assoc key = function
  ANil -> None
| ACell ({key = x; data = y}, rest) -> if x = key then Some y else assoc key rest

let rec assoc_if f = function
  ANil -> None
| ACell ({key = x; data = y}, rest) -> if f x then Some y else assoc_if f rest

どちらの関数もパターンマッチングでレコードから値を取り出します。レコードはフィールドに名前が付いているので、組と違ってデータの順番を気にする必要はなく、わかりやすいプログラムになると思います。

それでは、簡単な実行例を示します。

val acons : 'a -> 'b -> ('a, 'b) alist -> ('a, 'b) alist = <fun>
val pairlis : 'a list -> 'b list -> ('a, 'b) alist = <fun>
val assoc : 'a -> ('a, 'b) alist -> 'b option = <fun>
val assoc_if : ('a -> bool) -> ('a, 'b) alist -> 'b option = <fun>
# let a = acons 1 10 ANil;;
val a : (int, int) alist = ACell ({key = 1; data = 10}, ANil)
# let b = acons 2 12 a;;
val b : (int, int) alist =
ACell ({key = 2; data = 12}, ACell ({key = 1; key = 10}, ANil))
# let c = pairlis( [1, 2, 3, 4, 5], [11, 12, 13, 14, 15] );;
val c : (int,int) alist = ... 省略 ...

# assoc 4 c:;
- : int option = Some 14
# assoc_if (fun x -> x mod 2 = 0) c;;
^ : int option = Some 12

正常に動作していますね。


ソート (2)

●マージソート

ソートのお話です。今まで例題としてクイックソートと挿入ソートを取り上げました。データ数を N とすると、挿入ソートの実行時間は N2 に比例します。挿入ソートは遅いソートですが、クイックソートは高速なソートで、実行時間は N * log2 N に比例します。ところがクイックソートにも弱点があり、枢軸の選び方によっては実行時間が N2 に比例する遅いソートになってしまいます。リストの場合、枢軸を自由に選ぶことが難しいので、クイックソートはリスト向きのアルゴリズムとはいえません。

そこで、今回はリストに適したソートアルゴリズムである「マージソート (merge sort) 」を取り上げます。データ数を N とすると、マージソートの実行時間は N * log2 N に比例します。マージソートはクイックソートと同様に高速なアルゴリズムですが、実際にプログラムを作って比較してみるとクイックソートの方が高速になります。ただし、クイックソートと違って、データによって性能が劣化することはありません。どのようなデータに対しても力を発揮してくれます。

●リストのマージ

まず最初にマージから説明します。「マージ (併合) 」とは、複数のソート済みのリストを一つのソート済みのリストにまとめる操作です。次の図を見てください。

     ┌─ [1; 3; 5]  : リスト a 
[] ←┤
     └─ [2; 4; 6]  : リスト b 

   小さい方をセットする

      ┌─ [3; 5]    : リスト a 
[1] ←┘
           [2; 4; 6] : リスト b 

   1 をセットする

              [3; 5] : リスト a 
[1; 2] ←┐
         └─ [4; 6] : リスト b 

   2 をセットする

データがなくなるまで繰り返す 

    図 1 : リストのマージ

2 つのリスト a と b があります。これらのリストはソート済みとしましょう。これらのリストをソート済みのリストにまとめることを考えます。a と b はソート済みなので先頭のデータがいちばん小さな値です。したがって、上図のように先頭のデータを比較し、小さい方のデータを取り出して順番に並べていけば、ソート済みのリストにまとめることができます。途中でどちらかのリストが空になったら、残ったリストのデータをそのまま追加します。

それでは、実際にプログラムを作ってみましょう。次のリストを見てください。

リスト 9 : リストのマージ

let rec merge xs ys =
  match (xs, ys) with
    ([], _) -> ys
  | (_, []) -> xs
  | (x1:: xs1, y1::ys1) ->
    if x1 < y1 then x1::merge xs1 ys else y1::merge xs ys1

関数 merge の引数 xs と ys がマージするリストです。最初の節はリスト xs が空リストになった場合で、リスト ys をそのまま返します。2 番目の節はリスト ys が空リストになった場合で、リスト xs をそのまま返します。この 2 つの節が再帰呼び出しの停止条件になります。

リスト xs と ys にデータがあれば、先頭要素 x1 と y1 を比較します。x1 が小さい場合は x1 を、そうでなければ y1 を merge が返すリストに追加します。merge を再帰呼び出しするときは、追加する先頭要素をリストから取り除くことに注意してください。これでリストをマージすることができます。

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

val merge : 'a list -> 'a list -> 'a list = <fun>
# merge [1; 3; 5; 7] [2; 4; 6; 8];;
- : int list = [1; 2; 3; 4; 5; 6; 7; 8]
# merge [10; 20; 30] [1; 2; 3; 4];;
- : int list = [1,2,3,4,10,20,30]

●マージソートの実装

マージソートは、このマージを使ってデータをソートします。次の図を見てください。

 9 5 3 7 6 4 2 8  最初の状態

|5 9|3 7|4 6|2 8| 長さ2の列に併合

|3 5 7 9|2 4 6 8| 長さ4の列に併合 

 2 3 4 5 6 7 8 9  ソート終了

        図 2 : マージソート

マージをソートに応用する場合、最初は各要素をソート済みのリストとして考えます。この状態で隣のリストとマージを行い、長さ 2 のリストを作ります。次に、このリストに対して再度マージを行い、長さ 4 のリストを作ります。このように順番にマージしていくと、最後にはひとつのリストにマージされソートが完了します。

実際にプログラムを作る場合、リストの長さを 1, 2, 4, 8, ... と増やしていくよりも、再帰的に考えた方が簡単です。まず、ソートするリストを 2 つに分割して、前半部分をソートします。次に、後半部分をソートして、その結果をマージすればいいわけです。

再帰呼び出しするたびにリストは 2 つに分割されるので、最後にリストの要素はひとつとなります。これはソート済みのリストなので、ここで再帰呼び出しを終了してマージ処理を行えばいいわけです。プログラムは次のようになります。

リスト 10 : マージソート

(* 先頭から n 個の要素を削除する *)
let rec drop xs n =
  match (xs, n) with
    ((_, 0) | ([], _)) -> xs
  | (_ :: ys, _) -> drop ys (n - 1)

(* マージソート *)
let rec merge_sort n xs =
  match (n, xs) with
    (_, []) -> []
  | (1, x::xs) -> [x]
  | (2, x1::x2::xs) ->
    if x1 < x2 then [x1; x2] else [x2; x1]
  | (_, _) ->
    let m = n / 2 in
    merge (merge_sort m xs) (merge_sort (n - m) (drop xs m))

関数 merge_sort の引数 xs がソートするリスト、引数 n がリストの長さを表します。merge_sort はリストを分割する処理で、新しいリストを作らないことに注意してください。次の図を見てください。

 引数 x
  |
  |←── 長さn ──→|
(1 2 3 4 5 6 7 8)   
  |←n/2→| |←n/2→|
  |          |
 引数 x      引数 y     再帰呼び出し 

        図 3 : リストの分割

merge_sort はソートするリストの範囲を開始位置と長さで表しています。上図のリストを二分割する場合、前半部分は x と n / 2 で表し、後半部分を y と n / 2 で表します。y はリスト x の先頭から n / 2 個の要素を取り除けば求めることができます。この処理を関数 drop で行います。drop は list の先頭から n 個の要素を取り除きます。リストに対して n 回だけ tl を適用すると考えてもかまいません。簡単な例を示しましょう。

val drop : 'a list -> int -> 'a list = <fun>
# drop [1; 2; 3; 4] 0;;
- : int list = [1; 2; 3; 4]
# drop [1; 2; 3; 4] 2;;
- : int list = [3; 4]
# drop [1; 2; 3; 4] 4;;
- : int list = []

あとは再帰呼び出しでリストを分割していき、リストの長さが 1 になったならば新しいリストを返します。リストの長さが 2 の場合は簡単なので、2 つの要素を比較してソート済みのリストを作成して返します。そして、merge_sort の返り値を merge でマージすればいいわけです。

それでは実行してみましょう。

val merge_sort : int -> 'a list -> 'a list = <fun>
# merge_sort 9 [5; 9; 1; 8; 2; 7; 3; 6; 4];;
- : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9]

正常に動作していますね。


Copyright (C) 2008 Makoto Hiroi
All rights reserved.

[ PrevPage | OCaml | NextPage ]