OCaml プログラミング入門


基本

ここでは、型について述べます。

基本型

全ての値は型を持ちます。関数型言語では、 関数も(多少語弊がありますが)値と同じように扱えて、型を持ちます。 ここでは OCaml で使える基本的な型を簡単に説明して行きます。

unit 型

unit 型は、C で言う void みたいな物だと思っ て下さい。unit 型の値は一つしかなく、()で表 現します。

# ();;
- : unit = ()        ← 式を評価した結果が unit 型で、値は () だったと言う返事

bool 型

bool 型は、C++ にもありますが、それと一緒です。 true (トゥルー) と false (フォールス) の二値しかありません。 (ちょっと驚いたのですが、某所で、 false をフェイルスと読んでる人が結構いる (^^; と聞いたもので、 読み方もつけてみました)

# 1 + 1 = 4 / 2;;     (* 「=」 は比較演算子 *)
- : bool = true     ← 式を評価した結果が bool 型で、値は true だったと言う返事

# "mojiretu" = "dayo"
- : bool = false

int, float 型

int は整数型。float は浮動小数点数です。
# 1;;
- : int = 1         ← 式を評価した結果が int 型で、値は 1  だったと言う返事

# 5.3;;
- : float = 5.3
では計算を。ちょっと慣れないと面倒なのが、整数 (int 型) の演算と浮動小数点数 (float 型) の演算とでは、違う演算子を使わ なければならないと言う事です。これは、演算子の型が決まっている事と、 OCaml では C 等と違って暗黙の型変換を行わないからです。
# 1 + 3 * 2;;
- : int = 7

# 5.0 +. 1.3;;
- : float = 6.3

# 3 +. 5.6;;                   ← +. は float 用の足し算なのに、int 値を使っている
This expression has type int but is here used with type float

# (float_of_int 3) +. 5.6;;    ← float_of_int は int 型の値を float 型に変換してくれる関数
- : float = 8.6
他にも、*, -, / などの演算子もあります。 (float の場合は *., -., /.)

char, string 型

文字と文字列。C と違って、文字列型 (string) があります。 文字列型の値.[i] で、i 文字目 を取得できます。この時、添字の値が文字列長より長い場合は 例外が発生します。

# 'c';;
- : char = 'c'

# "mojiretsu";;
- : string = "mojiretsu"

# "mojiretsu".[5];;
- : char = 'e'

# "mojiretsu".[10];;
Exception: Invalid_argument "String.get".

tuple 型

tuple 型は任意の型の積の型を表わします。 tuple 値は( , ..., ) で新しく作成する事ができ、 その型は組合せた型を * で つないだものとして表わされます。

# (1, 3);;
- : int * int = 1, 3

# (35.3, "hoge", 'c', -2);;
- : float * string * char * int = 35.3, "hoge", 'c', -2

list, array 型

OCaml ではリストも配列も簡単に作れます。 リスト値は [ ; ... ; ] で、 配列は [| ; ... ; |] で作成 する事ができます。あるリストもしくは配列の 各要素は同じ型でなければなりません。 リストもしくは配列の型は、 要素の型が t だったとすると、 t list (リストの場合) もしくは t array (配列の場合) となります。

# [1; 2; 3];;
- : int list = [1; 2; 3]

# 'a'::['b';'c';'d'];;
- : char list = ['a'; 'b'; 'c'; 'd']
ここで、:: は要素をリストの先頭にくっつけて、 新しいリストを結果として返す演算子(関数)です。次は array です。
# [|3.2; 5.1; 9.2|];;
- : float array = [|3.2; 5.1; 9.2|]

# [|2.3; 5.9; 8.3|].(1);;
- : float = 5.9

配列の場合は、配列型の値.(i)で、i 要素目を取得できます。 この時、添字 i が配列の要素数より大きかった時には例外が 発生します。

list と array は構文的にとても似ている気がしますが、 中身の実装が結構違うため、 それぞれのデータ構造の上の操作が、操作の種類によって かかる時間が違う場合があります。 例えば、array では任意の場所の要素を .() で参照できますが、list の場合は先頭から 順に辿って行く必要があるため、list が長ければ長い程時間がかかります。 C++ の STL の list と vector に対応すると考えて良いでしょう。

型を定義する

「type 型名 = 型」と言う構文で新しく型に名前をつけられます。 文法上の決まりから、型名は小文字で始まる 必要があります。 例えば、虚数を実数部と虚数部の組として表現したとして、

# type imaginary_number = float * float;;
type imaginary_number = float * float
のように定義する事です。ただし、これは別名をつけているだけで、 imaginary_number 型が使えるところには、float * float 型が使えてしまうので、このように使うと若干安全性が落ち るかもしれません。

モジュールのインターフェースの定義で型を 隠蔽すれば、モジュールを使う人にとって安全にする事は可能です

variant 型

variant 型は、C の union みたいなものですが、 特徴は「タグ」をつける事ができ、union に格納されて いるデータはどの型なのかと言うのが簡単にわかる仕組 になっています。 今まで挙げた型はある意味自動的に作られましたが、 variant 型には名前をつけて、あらかじめ宣言をして おかなければなりません。ここでも文法上の決まりから、型の名前は小文字で 始まる必要があり、また、タグの名前は大文字で始める 必要があります。タグ毎に、| で区切っていきます。

C の enum 型みたいに使ったり:

# type day = Mon | Tue | Wed | Thu | Fri | Sat | Sun;;
type day = Mon | Tue | Wed | Thu | Fri | Sat | Sun
「タグ名 of 型」で、各タグにデータを持たす事ができます
# type angle = Degrees of float | Radian of float
type angle = Degrees of float | Radian of float

variant 型を使うと、再帰データ型が以下のように簡単に定義できます。

# type 'a bintree = Leaf of 'a | Node of 'a bintree * 'a bintree;;
type 'a bintree = Leaf of 'a | Node of 'a bintree * 'a bintree

# Leaf 3;;
- : int bintree = Leaf 3

# Node ((Leaf "abc"), (Node ((Leaf "def"), (Leaf "ghi"))));;
- : string bintree = Node (Leaf "abc", Node (Leaf "def", Leaf "ghi"))

ここで、'a は型変数と呼ばれるもので、 どんな型をあてはめても良いよ、と言う事をあらわしています。上では 最初の例では 'aint 型になり、全体として は int bintree 型になっており、 二番目の例では string bintree 型になっているのがわかります。

variant 型は、後程説明するパターンマッチングと合わさって、 バグの無いコードを簡単に作る事に関して絶大な威力を発揮します。

record 型

C で言う struct みたいなものです。record 型もあらかじめ宣言しておか ないと使えません。 「type 型名 = {フィールド名1:型1; フィールド名2:型2; ...}」で定義しま す。フィールド名は小文字で始まる 必要があります。 struct と同様に、「record型の値.フィールド名」 で 要素の値を参照できます。

# type point = { x:float; y:float; color:int }
type point = { x : float; y : float; color : int; }
# let p = {x=3.5; y=2.8; color=3};;
val p : point = {x = 3.5; y = 2.8; color = 3}
# p.x *. p.y;;
- : float = 9.8

C の struct と微妙に違う点は、record 型の値を生成する時は、 全ての要素の値を指定しなければならない事、また、 二つの違う record 間で、同じフィールド名があってはならない (正確には、あっても良いが、後に書いた方が使われる。 別モジュールならば名前の衝突が起らないので構わない)。 これは一見変な制限ですが、仮に、上の point と言うレコード 型 の他に、type onex ={ x : int } と 言う別の record 型があったとしたら、 let f hoge = hoge.x とした時に hoge の型は point なのか onex なのか、 型推論ができなくなってしまうため、このような仕様となっています。

関数型

関数の型はどう考えるのか? 一番綺麗な考えかたは、 「全ての関数は引数が一つで、返り値も一つ」 とする事です。 引数を 2 つ以上持たせたい時はどうするの?って事はひとまず置いといて、 1 引数の場合だけを考えましょう。

関数 f の引数の型が t1、 返り値の型が t2 の時、f の 型を t1 -> t2 とあらわします。 関数に引数を与えて、評価する事を 「関数適用」と呼びます。 関数の適用は、関数名のあとに、引数を並べるだけです。 C 言語などと違って、括弧「(、)」を必要としません。

# succ;;                   ← 引数 + 1 を返す標準ライブラリの関数
- : int -> int = <fun>     ← 引数が int 型で、返り値も int 型の関数型の値

# succ 3;;
- : int = 4

OCaml では、関数とは他の値と同様に、「関数型を持つ値」 として扱えます。 OCaml では関数型の値を幾つかの方法で定義できますが、 引数が一つで、返り値も一つと言うような関数を表わすための 構文は function 引数 -> 関数本体と言う というものです。この構文によって、関数型の値が生成されるので、それ を適当な変数に束縛する事で、その変数の名前で関数を使える事になります。 いちいち function と打つのはながったらしいので、 省略形もあります。 以下の表現は同値で、引数に 5 を足した 結果を返す関数 (と言う値) を f に束縛します。

# let f5 = function x -> x + 5;;
val f5 : int -> int = <fun>

# let f5 x = x + 5;;      ← 上の省略形。こっちをもっぱら使う
val f5 : int -> int = <fun>

# f5 3;;
- : int = 8

では、引数を二つ持ちたい時はどうしましょう? 答は簡単で、 「function x1 -> function x2 -> ... function xn -> 関数本体」と言う風に、順番に引数を受けとるような形の関数を 定義すれば良いわけです。これではあまりにも長ったらしいので、また 省略形があり、「fun x1 x2 ... xn -> 関数本体」 とも書けます。以下の 3 つの文は同じ事を違う書き方で書いただけです。

# let addxy = function x -> function y -> x + y;;
# let addxy = fun x y -> x + y;;
# let addxy x y = x + y;;
val addxy : int -> int -> int = <fun>

この時、addxy の型は、最初の引数が int であ るという事と、関数型の型は t1 -> t2 と言う形をしているという事とを考えると、 int -> (int -> int) である事になります。 つまり、addxy は最初の引数を与えると返り値として、 「int型の値を引数として、int型の値を返す関数」 を返す事がわかります。 実際、二つの引数を適用する時は、 addxy 3 8 と言うのは ((addxy 3) 8) と言う意味にな ります。 となると気付く事がありますが、 引数が二つ以上の関数には、引数を全部与えなくても良い 事がわかります。この事を「関数の部分適用」と言います。

# let add3y = addxy 3;;            ← 部分適用
val add3y : int -> int = <fun>
# add3y 8;;
- : int = 11

部分適用によって、特定の引数だけが既に指定された関数が簡単に作れます。

例外型

型なのだろうか (^^;?とにかく OCaml でも例外が使えます。 理論的な事はわかりません :P。 定義の方法は variant 型と似ていますが、| は使えません。 どんな例外も全部 exn 型?となる様です。

どうでも良い例:

# exception Mona of string;;
exception Mona of string

# let say s =
    if s = "itteyoshi" then
      raise (Mona "omaemona-")
    else
      print_string s;;
val say : string -> unit = <fun>

# say "2get!!!\n";;
2get!!!
- : unit = ()

# say "itteyoshi";;
Exception: Mona "omaemona-".

ref 型

ref: 'a -> 'a ref と言う関数があり、 この関数を使うとどんな型の値から、その参照 (ポインタみたいなもの) を得る事ができます (C などみたいなポインタ演算などはできません)。 関数的でない、副作用のある破壊的代入がしたい時にはこの型を使います。

# let a = ref 0;;
val a : int ref = {contents = 0}
#   a := 1;;
- : unit = ()
# !a;;
- : int = 1

ここで、:= が代入演算子、 ! が参照値から、その参照している値を返す演算子となります。 最初のうちはなるべく使わないでプログラムを書くようにするのが、 関数型プログラミングに慣れるコツだと思います。 実は上で述べている、array 型、string 型、record 型にも破壊的な 代入の構文がありますが、とりあえずここでは述べない事にします。