OCaml に入門してみた。その1

Akihito Tanaka
all worldly things are transitory.
13 min readSep 23, 2018
“group of people riding camels on desert” by Yeo Khee on Unsplash

最近、プログラミングしながら色々と思うことがあり、OCamlを触ってみようと思い立った。とりあえず OCamlのマニュアルを読み始めてみたので、そのログというか、雑な翻訳みたいなものを残しておこうと思う。

とりあえず今日は1章 The Core language1.4 Records and variantsまで。

The Core language

1.1 Basics

OCaml の REPL では OCaml の文を書いて ;;で終了すると、コンパイル、実行され、結果が返ってくる。

# 1 + 2 * 3;;
- : int = 7
# let pi = 4.0 *. atan 1.0;;
val pi : float = 3.14159265358979312
# let square x = x *. x;;
val square : float -> float = <fun>
# square (sin pi) +. square (cos pi);;
- : float = 1.

OCaml は各文の値と型を計算する。関数のパラメータも、関数内での利用方法から型を推論するので明示的に指定する必要がない。float と integer は型が区別されるだけでなく、演算子も区別される。integer なら +*、float なら +.*.

再帰関数は let recを使って定義する。

# let rec fib n =
if n < 2 then n else fib (n-1) + fib (n-2);;
val fib : int -> int = <fun>
# fib 10;;
- : int = 55
# fib 0;;
- : int = 0
# fib 1;;
- : int = 1

1.2 Data types

OCaml の基本データ型には次のものがある。

  • int
  • float
  • boolean
  • char
  • string
# (1 < 2) = false;;
- : bool = false
# 'a';;
- : char = 'a'
# 'Hello world';;
Error: Syntax error
# "Hello world";;
- : string = "Hello world"

char はシングルクォート、 string はダブルクォートでくくるらしい。

定義済みデータ構造は次のものがある。

  • tuple
  • array
  • list

もちろん自分でデータ構造を定義するための仕組みもある。

定義済みデータ構造の一つ、list を生成するには、 [] で囲って、要素を ;で区切る。list の先頭に要素を追加するためには ::(cons) を使う。

# let l = ["is"; "a"; "tale"; "told"; "etc."];;
val l : string list = ["is"; "a"; "tale"; "told"; "etc."]
# "Life" :: l;;
- : string list = ["Life"; "is"; "a"; "tale"; "told"; "etc."]

当然、list の要素の型は揃える必要がある。

# let m = ["is"; 'a'; "tale"; "told"; "etc."];;
Error: This expression has type char but an expression was expected of type
string

他の OCaml のデータ構造と同様に list は明示的にメモリーの確保・開放する必要はない。OCaml ではメモリーの管理は自動的に行われる。明示的にポインタを扱うこともない。

OCaml の多くのデータ構造では、パターンマッチで中身を調査、展開できる。list のパターンマッチは list の式と同じ表現で指定できる。

# let rec sort lst =
match lst with
[] -> []
| head :: tail -> insert head (sort tail)
and insert elt lst =
match lst with
[] -> [elt]
| head :: tail -> if elt <= head then elt :: lst else head :: insert elt tail
;;
val sort : 'a list -> 'a list = <fun>
val insert : 'a -> 'a list -> 'a list = <fun>
# sort l;;
- : string list = ["a"; "etc."; "is"; "tale"; "told"]
# let m = ["b"; "c"; "d"];;
val m : string list = ["b"; "c"; "d"]
# insert "g" m;;
- : string list = ["b"; "c"; "d"; "g"]
# insert "a" m;;
- : string list = ["a"; "b"; "c"; "d"]

sort の型推論結果 'a list -> ‘a listは、 sortは任意の型の list を引数にとり、同じの型の list を返すことを意味している。 'aは型変数である。比較演算子は OCaml ではポリモーフィックなので、 sortは任意の型のリストに適用することができる。

# sort [6;2;5;3];;
- : int list = [2; 3; 5; 6]
# sort [3.14;2.718];;
- : float list = [2.718; 3.14]

sort は入力の list を変更せず、昇順にソートされた新しい list を作って返す。OCaml では一度作った list を変更する方法はない。イミュータブルなのである。多くの OCaml のデータ構造はイミュータブルだが、2、3つはミュータブルなものもある。

Ocaml の複数の引数を取る関数の型の記法は次のようになる。

arg1_type -> arg2_type -> … -> return_type

例えば、 insert'a -> ‘a list -> ‘a listと定義されるが、これは任意の型の値とその型を要素に持つ list を引数に取り、入力と同じ型を持つ list を返すことを意味する。

1.3 Functions as values

OCaml は関数型言語であり、関数を他のデータと同様に自由に渡すことができる。

# let deriv f dx = function x -> (f (x +. dx) -. f x) /. dx;;
val deriv : (float -> float) -> float -> float -> float = <fun>
# let sin' = deriv sin 1e-6;;
val sin' : float -> float = <fun>
# sin' pi;;
- : float = -1.00000000013961143

つまり、関数合成できる。

# let compose f g = function x -> f(g x);;
val compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun>
# let cos2 = compose square cos;;
val cos2 : float -> float = <fun>
# cos2 pi;;
- : float = 1.

関数を引数にとれる関数のことを「ファンクショナルな関数」、「高階関数」と呼ぶ。高階関数はイテレータやデータ構造をまたいだジェネリックなオペレーションを生成するのに便利。

OCaml ライブラリの List.mapは list の各要素に与えられた関数を適用し、その結果を返す。

# List.map (function n -> n * 2 + 1) [0;1;2;3;4];;
- : int list = [1; 3; 5; 7; 9]

この mapは次のように定義できる。

# let rec map f l =
match l with
[] -> []
| hd :: tl -> f hd :: map f tl;;
val map : ('a -> 'b) -> 'a list -> 'b list = <fun>
# map (function n -> n * 2 + 1) [0;1;2;3;4];;
- : int list = [1; 3; 5; 7; 9]

1.4 Records and variants

ユーザー定義データ構造にはレコードと variant がある。どちらも typeをつかって定義できる。

以下は分数を型で定義した例である。

# type ratio = {num: int; denom: int};;
type ratio = { num : int; denom : int; }
# let add_ratio r1 r2 =
{num = r1.num * r2.denom + r2.num * r1.denom;
denom = r1.denom * r2.denom};;
val add_ratio : ratio -> ratio -> ratio = <fun>
# add_ratio {num=1; denom=3} {num=2; denom=5};;
- : ratio = {num = 11; denom = 15}

レコードのフィールドにはパターンマッチでアクセスできる。

# let integer_part r =
match r with
{num=num; denom=denom} -> num / denom;;
val integer_part : ratio -> int = <fun>

パターンマッチに1ケースしかない場合であれば、次のように引数 rをレコードのパターンで直接展開しても安全である。

# let integer_part {num=num; denom=denom} = num / denom;;
val integer_part : ratio -> int = <fun>

不要なフィールドは省略することもできる。

# let get_denom {denom=denom} = denom;;
val get_denom : ratio -> int = <fun>

オプションで、フィールドが欠落していることをフィールドのリストの末尾をワイルドカード _で終えることで明示することができる。

# let get_num {num=num; _ } = num;;
val get_num : ratio -> int = <fun>

レコードを生成する際に次のようにフィールドを略記することもできる。

# let ratio num denom = {num; denom};;
val ratio : int -> int -> ratio = <fun>

レコードのいくつかのフィールドを同時に更新することもできる。

# let integer_product integer ratio = { ratio with num = integer * ratio.num };;
val integer_product : int -> ratio -> ratio = <fun>

このようなファンクショナルな更新記法を用いると、 withより右手の更新されるフィールド以外は左手のレコードからコピーされる。

variant 型の宣言はその variant 型で値が取り得る全ての型を列挙する。それぞれのケースは名前によって区別され、コンストラクタと呼ばれ、 variant 型の値の生成とパターンマッチによる検査を提供する。コンストラクタの名前は変数名と区別するために先頭大文字にする。

次の例は、整数と浮動小数点数をミックスした variant 型である。

# type number = Int of int | Float of float | Error;;
type number = Int of int | Float of float | Error

この宣言は number型は整数か浮動小数点数、または不正な操作結果を意味する定数 Errorを表現している。

取り得る定数を示した列挙型は variant 型の特別なケースである。

# type sign = Positive | Negative;;
type sign = Positive | Negative
# let sign_int n = if n >= 0 then Positive else Negative;;
val sign_int : int -> sign = <fun>
# sign_int 1;;
- : sign = Positive
# sign_int (-1);;
- : sign = Negative

number 型に対する算術的な演算を定義するために、2つの数にパターンマッチを使うと次のようになる。

# let add_num n1 n2 =
match (n1, n2) with
(Int i1, Int i2) ->
(* Check for overflow of integer addition *)
if sign_int i1 = sign_int i2 && sign_int (i1 + i2) <> sign_int i1
then Float(float i1 +. float i2)
else Int(i1 + i2)
| (Int i1, Float f2) -> Float(float i1 +. f2)
| (Float f1, Int i2) -> Float(f1 +. float i2)
| (Float f1, Float f2) -> Float(f1 +. f2)
| (Error, _) -> Error
| (_, Error) -> Error;;
val add_num : number -> number -> number = <fun>

他の variant 型についての興味深い例は組み込みの型 'aの値か値が無いことを表す 'a option型である。

# type 'a option = Some of 'a | None;;
type 'a option = Some of 'a | None

この型は一般的なシチュエーションで失敗する可能性のある関数を定義するときにとても役立つ。

# let safe_square_root x = if x > 0. then Some(sqrt x) else None;;
val safe_square_root : float -> float option = <fun>
# safe_square_root 2.;;
- : float option = Some 1.41421356237309515
# safe_square_root (-2.);;
- : float option = None

最も一般的な variant 型の使い方は再帰的なデータ型の表現である。例えば、バイナリツリー型を考えてみる。

# type 'a btree = Empty | Node of 'a * 'a btree * 'a btree;;
type 'a btree = Empty | Node of 'a * 'a btree * 'a btree

この定義は、任意の型 'aを持つバイナリツリーは空か、型 'aの1つの値と2つの型 'aの値を持つサブツリーを持つノードを持つと定義される。

バイナリツリーの操作は再帰関数で表現される。次は整列されたバイナリツリーをルックアップ、挿入する操作を行う例である。

# let rec member x btree =
match btree with
Empty -> false
| Node(y, left, right) ->
if x = y then true else
if x < y then member x left else member x right;;
val member : 'a -> 'a btree -> bool = <fun>
# let rec insert x btree =
match btree with
Empty -> Node(x, Empty, Empty)
| Node(y, left, right) ->
if x <= y then Node(y, insert x left, right)
else Node(y, left, insert x right);;
val insert : 'a -> 'a btree -> 'a btree = <fun>

とりあえず今日はここまで。1.5章以降は後日。

ここまでで出てきた例は一通り理解できたように思う。構文が Scala に似てるところもちらほらあるような。むしろ、 Scala が OCaml の影響を受けているのか。

間違い等あれば、コメントください。

--

--