2019年10月6日
[ 前の資料 | 講義ホームページ・トップ | 次の資料 ]
OCaml (旧名称: Objective Caml)は INRIA というフランスの国立の計算機科学の研究所でデザイン・開発された言語である.歴史的には,証明支援系と呼ばれる,計算機によって数学的証明を行うためのソフトウェアを操作するための言語の研究から発展してきたもので,当初は ML (Meta Language) と呼ばれていたが,研究が進展し,また方言が派生していく過程で OCaml ができあがった.Standard ML などの方言をまとめて ML と呼ぶことが多い.(興味があったら Caml の歴史, Standard MLの歴史 などを見よ.)
ML は関数型プログラミング(functional programming)と呼ばれるプログラミングスタイルをサポートしている.MLは核となる部分が小さくシンプルであるため,プログラミング初心者向けの教育用に適した言語である.と同時に,大規模なアプリケーション開発のためのモジュール化機能が充実している.MLの核言語は型付きラムダ計算と呼ばれる,形式的な計算モデルに基づいている.このことは,言語仕様を形式的に(数学的な厳密な概念を用いて)定義し,その性質を厳密に「証明」することを可能にしている.実際,Standard ML という言語仕様においては,(コンパイラの受理する)正しいプログラムは決して未定義の動作をおこさない,といった性質が証明されている.
OCaml は Standard ML とは構文が異なるが,ほとんどの機能は共有している.また,OCaml では Standard ML には見られない,独自の拡張が多く施されており,関数型プログラミングだけでなく,オブジェクト指向プログラミングもサポートされている.また効率的なコードを生成する優れたコンパイラが開発されている.
OCamlの概要をプログラム例を混じえて紹介する.二分探索木がプログラムできるための最短ルートを通るため,トップページで紹介している他の資料にもあたってもらいたい.
OCaml には入力を即座に実行して結果を表示する対話的処理系が用意されていて,インタラクティブにプログラムの実行を進めることができる1. OCamlでは入力された式を値に評価することでプログラムの実行が進むので,対話的処理系は電卓のようなものである.
#
が処理系による入力プロンプトの記号で以降が実際の入力である.;;
は電卓の=キーのようなもので,式の入力の終わりを告げるための記号である.次の行に入力された数式の計算結果(値と呼ぶ) 7
が表示されている.int
は式の型を示している.
このように比較演算の結果は true
, false
というbool
型の定数で表される.等号は ==
ではなく =
である.また,不等号は <>
である2. 基本的なデータとしては,他にも浮動小数点数の float
型,文字列のstring
型などがあるがここでは紹介しない.
OCaml ではlet宣言を使って式の計算結果に名前をつけることができる.OCaml ではこの名前のことを変数と呼ぶ.
val
は値を表す名前が宣言されたことを表している.(型を定義すれば val
ではなく type
になったりする.)
これは Java であれば,変数宣言(と初期化)
に相当すると考えられるが,いくつか違いがある.
int x;
のように変数の宣言だけをする,ということはできない.: 〈型〉
」を書く.「これは代入ではないのか?」と思うかもしれない.それに対する説明はもうすぐ行う.
OCaml の主要なプログラム構成要素は関数である.関数も let
を使って宣言する.関数の定義の構文は,
となる.〈パラメータ1〉〜〈パラメータn〉 の値を受け取って,〈式〉を計算してその値を返す.特に return
などを使う必要はなく計算式のみ書けばよい.例を見てみよう.
# let average(x, y) = (x + y) / 2;;
val average : int * int -> int = <fun>
# average(5, 7);;
- : int = 6
# average(10, 3);;
- : int = 6
関数の型は 〈引数1の型〉* ... *〈引数nの型〉-> 〈返値型〉
と表現される.関数についても,パラメータや返値の型を書く必要はなく,型推論が行われて関数 average
は2つの整数を取って,整数を返すことがわかる.数式と違って計算結果(=
の右側)は単に<fun>
と表示されているが,これは何らかの関数であることを示している.
パラメータの数がひとつの時は括弧を省略することができ,そして,ふつうは省略する(ただし,関数名との間には空白が必要である).
条件によって異なる値を計算したい時にはif式を使う.if式は
という形で,〈条件〉(これは bool
型の式でなければならない)が true
ならば,〈式1〉 の値が,false
ならば〈式2〉の値がif式全体の値になる,というものである.例えば,絶対値を計算する関数 abs
は以下のように書ける.
# let abs n =
if n < 0 then -n else n;;
val abs : int -> int = <fun>
# abs(-100);;
- : int = 100
# abs 20;;
- : int = 20
-100
のまわりの括弧は必要である.これがないと,OCamlは abs -100
を「abs
引く100」と解釈してしまい,型についてのエラーが出てくる.
再帰関数を宣言する時には,let
ではなく let rec
と書く.(rec
は「再帰」を表す recursion からとっている.)例えば,階乗(factorial)を計算する関数は
# let rec fact n =
if n = 1 then 1 else n * fact (n - 1);;
val fact : int -> int = <fun>
# fact 5;;
- : int = 120
のように定義できる.ここでも n - 1
のまわりの括弧も必要である.これがないと,OCamlはこの式を「(fact n) - 1
」と解釈してしまう.(fact n-1
とマイナスの前後の空白を詰めて書いても無駄である.)
fact
に 0
(や負数)を与えた場合は,この計算は
fact 0
→ 0 * fact (-1)
→ 0 * (-1) * fact (-2)
→
となって止まらず,ついには(式が大きくなり過ぎて)メモリが足りなくなって overflow エラーになってしまう.
関数の定義中で使ってよい変数はパラメータだけではない.それまでに宣言された変数を使うことができる.以下は,与えられた半径から円の面積を計算する関数 circle_area
の定義である.(*.
は float
のための乗算演算子である.)
# let pi = 3.14;;
val pi : float = 3.14
# let circle_area radius = pi *. radius *. radius;;
val circle_area : float -> float = <fun>
# circle_area 4.0;;
- : float = 50.24
circle_area
の中で pi
が使われている.
さて,上で,同じ変数名の定義が複数あってもそれは代入ではない,といったが,それは以下のような例から確認できる.
関数定義中のpi
の使用は,本体を計算する時点での pi
の値をとってくるのではなく,関数が定義された時点での pi
の値を使うことになる.一方で,以下のような計算をすると再定義された pi
の値 3.0
が引数になるし,
(新しい pi
を)関数定義で使えば,円周率が 3.0 となる面積計算になる.
# let circle_area2 radius = pi *. radius *. radius;;
val circle_area2 : float -> float = <fun>
# circle_area2 4.0;;
- : float = 48.
(48.
の最後の小数点は整数定数表記 48
と区別するために表示される.48.0
と同じ扱いである.)
このように OCaml では,変数の使用とそれに対応する定義は,使用箇所から上に辿って一番近いものになり,変数を使った計算が起こる瞬間にその名前の値が何であるか,とは関係がない.このような,プログラムテキスト中での字面上の関係を使って使用と定義の対応を決める方式を,静的スコーピング(static scoping),という.
let を式の中で使うことによって,式の計算中,値に一時的に(局所的に)名前をつけることができる.これが「let 式」である.let式の構文は
であり,直観的には「〈式2〉の計算中,〈変数〉を〈式1〉(の値)とする」という意味で,〈式1〉を計算し,その値に〈変数〉という名前をつけ,〈式2〉の値を計算する.
この〈変数〉の有効範囲は〈式2〉だけで,〈式2〉の外側では使用することができない.
let式もただの式の一種なので,式が書けるところでは自由に使うことができる.典型的には,〈式2〉にletを使うことで複数の変数を順に定義していくように書ける.
また,以下のように,関数定義の本体で let を使うこともできるし,さらには,関数を局所的に定義することもできる.
# let cylinder_vol(r, h) =
let bottom = circle_area r in (* 底面積の計算 *)
bottom *. h;;
val cylinder_vol : float * float -> float = <fun>
# let cube n = n * n * n in
cube 1 + cube 2 + cube 3;;
- : int = 36
# cube;;
Characters 0-4:
cube;;
^^^^
Error: Unbound value cube
二番目の例では,\(1^3 + 2^3 + 3^3\) を計算するために,まず(局所的に)3乗関数を定義している.この式の外側では cube
は使用できない.(途中の (* ... *)
は OCaml のコメント記法である.)
与えられた正整数 \(n\) に対し (\(1^2 + 2^2 + \cdots + n^2\)) を計算する再帰関数 squaresum : int -> int
を定義せよ.(引数\(n\)が負である場合の動作はどう定義してもよい.)
与えられた正整数 \(n\) に対し,\(n\) 番目のフィボナッチ数を計算する再帰関数 fib : int -> int
を定義せよ.(引数\(n\)が負である場合の動作はどう定義してもよい.)
ユークリッドの互除法を使ってふたつの正整数 \(n\), \(m\) の最大公約数を計算する再帰関数 gcd : int * int -> int
を定義せよ.(引数\(n\)が負である場合の動作はどう定義してもよい.)
(解答3)
OCaml では,type
宣言を使って様々な複合的なデータ型を定義することができる4.
レコード(record)は,いくつかの値を(それぞれに名前をつけて)まとめたような値である.例えば二次元座標(座標は整数値)を表すレコード型 point
は以下のように定義できる.
x
, y
をレコードのフィールド(field)名と呼ぶ.レコード型の値を作るには,
などとする.(OCaml の文法は,最後のフィールドに関してやや寛容で,型定義にしてもレコードを作るにしても,最後のセミコロンはあってもなくてもよい.表示の仕方も型定義とレコード値でぶれているのが面白い.)
レコードから各フィールドの値を取り出すには,ふたつの方法がある.ひとつは ドット記法(dot notation) と呼ばれる方法で,<レコード式>.<フィールド名>
と書く.
# origin.x;;
- : int = 0
# let middle(p1, p2) =
{x = average(p1.x, p2.x); y = average(p1.y, p2.y) };;
val middle : point * point -> point = <fun>
# middle(origin, {x=4; y=5});;
- : point = {x = 2; y = 2}
もうひとつはパターンマッチ(pattern match)と呼ばれる機構を用いる方法である.パターンとは,構造を持つ値の形の骨組と情報を取りだしたい部分を指定する記法である.let
などの変数を宣言する機構と組み合わせて使うことができる.
例:
{x = ox; y = oy}
がパターンで,ox
, oy
は取り出したフィールドの値につける名前である.origin
の値の形と照合を行い変数に対応する部分が取り出されている.先程の middle
は以下のように書くこともできる(これだと余りパターンマッチを使う恩恵がないが).
# let middle(p1, p2) =
let {x = p1x; y = p1y} = p1 in
let {x = p2x; y = p2y} = p2 in
{x = average(p1x, p2x); y = average(p1y, p2y) };;
val middle : point * point -> point = <fun>
また,関数のパラメータに直接パターンを書くこともでき,
# let middle({x = p1x; y = p1y;}, {x = p2x; y = p2y;}) =
{x = average(p1x, p2x); y = average(p1y, p2y) };;
val middle : point * point -> point = <fun>
と書いてもよい.フィールドが沢山あるレコードから,一部のフィールドの値を取り出したい場合には,不要なフィールドについては何も書かなくてよく,また,フィールド値の名前としてフィールド名そのものを使う場合には =
以下を省略することができるので point
の x 座標を取り出す関数は
のように書ける.
ふたつの点を引数として,その二点を直径とする円の面積を返す関数 area : point * point -> float
を定義せよ.整数 n
は float_of_int
という関数を呼ぶことで float
に変換することができる.平方根は sqrt
関数で計算できる.
有理数を表現する型 rational
を以下のように定義する.
例えば「5分の4」は {num=4; den=5;}
と書くことになる.ふたつの有理数の和を求める関数 sum : rational * rational -> rational
を定義せよ.
(解答5)
ヴァリアント(variant)は,異なるデータを混ぜて使うための仕組みである.これは応用範囲が非常に広く,OCaml でデータを定義する,といったら9割がこの仕組みを使っている(ような気がする).
まず,一番単純な場合として,いくつか自前の定数を作ってそれらを混ぜて使うことを考える.以下は,ふりかけのデータ型 furikake
である.
# type furikake = Shake | Katsuo | Nori;;
type furikake = Shake | Katsuo | Nori;;
型furikake
の要素は Shake
, Katsuo
, Nori
の3つのみとなる.
# Shake;;
- : furikake = Shake
OCaml ではこの自前の定数をコンストラクタ(constructor)と呼ぶ.コンストラクタの名前は英大文字で始める必要がある.(Standard ML では英小文字でもよい.)
このfurikake
のように,定数を列挙することで構成されるデータ型を 列挙(enum)型 と呼び,Java や C では列挙型を定義するための専用構文がある.
さて,ヴァリアントについての処理の基本は場合分けである.
例えば,与えられたふりかけが菜食主義者でも食べられるかを判定するisVeggie
関数を考えてみよう.この関数は,ふりかけの種類によって3通りに場合分けを行うはずである.ヴァリアントについて条件分岐を行うには,match式を使う.以下が isVeggie
関数の定義である.
# let isVeggie f =
match f with
Shake -> false
| Katsuo -> false
| Nori -> true
;;
val isVeggie : furikake -> bool = <fun>
# isVeggie Shake;;
- : bool = false
match式は
という構文で,〈式0〉の値を〈パターン1〉から順に照合していき,i番目でマッチしたら(形があったら),〈式i〉を計算しその値がmatch式全体の値となる.if式は実は
と書ける6.今回の例では〈パターンi〉は単にコンストラクタの名前となっているが,もっと複雑なヴァリアントの場合には,パターンも複雑なものがでてくる.match式には,パターンのexhaustiveness check といって,〈パターン1〉から〈パターンn〉が〈式0〉の値としてありうる全ての場合を尽くしているかをチェックしてくれる機能がある.上の isVeggie
で f
の型は furikake
なので,3つのパターンを用意しないとコンパイラが警告を出す.下のように,例えば Katsuo
の場合を書かずに定義すると,定義はできるものの,「マッチ対象の値(つまりf
)が Katsuo
だとマッチしないよ」という警告が出る.
# let isVeggie f =
match f with
Shake -> false
| Nori -> true
;;
Characters 21-73:
....match f with
Shake -> false
| Nori -> true
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
Katsuo
val isVeggie : furikake -> bool = <fun>
また,パターンには,整数定数を書くこともでき,Java の switch
文のような処理も記述できる.例えば n
が20以下の素数ならば true
となり,その他の場合 false
になるような式は
match n with
2 -> true
| 3 -> true
| 5 -> true
| 7 -> true
| 11 -> true
| 13 -> true
| 17 -> true
| 19 -> true
| _ -> false
と書くことができる.最後の行の _
は ワイルドカード・パターン(wildcard pattern) と呼ばれ,何にでもマッチする(整数以外でも使える).上の例のように,複数のパターンで同じ式を計算する場合には,パターンをまとめて記述することができる.
2 | 3 ... | 19
は or パターン(or pattern) と呼ばれ一般的には,<パターン1> | <パターン2> | ... | <パターンn>
という形をして「いずれかのパターンにマッチするならば」という条件を表現することができる.
OCaml のヴァリアントはコンストラクタ毎に異なる種類の値を付与することで,異なる種類の値をひとつの型に混ぜて使うことができる.(むしろ,異なる値を混ぜて使うのが本来のヴァリアントである.)例えば,一品料理の型を考えてみよう.一品料理は「トンカツ」「味噌汁」「ふりかけごはん」のいずれかとするが,味噌の種類は「赤」「白」「あわせ」,味噌汁の具は「わかめ」「とうふ」「大根」のうちからひとつが選べ,ごはんのふりかけも選べるようにする.このような型の定義として,トンカツと9種類の味噌汁,3種類のふりかけごはんの計13種類のコンストラクタから成る列挙型も考えられるが,ここでは3種類のコンストラクタ PorkCutlet
, Soup
, Rice
を用意し,Soup
には味噌と具の種類の情報を,Rice
にはふりかけの情報を付与できるようにする.
# type miso = Aka | Shiro | Awase;;
type miso = Aka | Shiro | Awase
# type gu = Wakame | Tofu | Radish;;
# type dish = PorkCutlet | Soup of {m: miso; g: gu} | Rice of furikake;;
type dish = PorkCutlet | Soup of { m : miso; g : gu; } | Rice of furikake
of
以下には型の名前を書いて(int
や,この例のようにレコードでもよい7),各コンストラクタに何の情報が付与されるかを指定する.具体的な一品料理は,コンストラクタの後に(関数呼出しの引数のように)付帯情報を書く.
PorkCutlet;; (* トンカツ *)
- : dish = PorkCutlet
# Soup {m = Aka; g = Tofu};; (* 豆腐赤だし *)
- : dish = Soup {m = Aka; g = Tofu};;
# Rice Shake;; (* 鮭ふりかけごはん *)
- : dish = Rice Shake
どの式も型は dish
になっていることに注意すること.
引数を取るコンストラクタを単体で使うことはできない.
Rice;;
Characters 0-4:
Rice;;
^^^^
Error: The constructor Rice expects 1 argument(s),
but is applied here to 0 argument(s)
さて,与えられた dish
が固形かどうかを判定する関数 isSolid
を書いてみよう.これは,isVeggie
と同じように match
を使って記述するが,引数を取るコンストラクタについては引数につける名前を同時に指定する.
# let isSolid d =
match d with
PorkCutlet -> true
| Soup m_and_g -> false
| Rice f -> true
;;
val isSolid : dish -> bool = <fun>
この例では f
などの付帯情報を表す変数は全く使っていない(ので,m_and_g
や f
の代わりにワイルドカード _
を書いてもよい.)が,対応する,->
の右側の式で使うことができる.例として,料理の値段を計算する関数を定義してみよう.まず,以下が値段表である.
一品料理 | 値段(円) | 備考 |
---|---|---|
トンカツ | 350 | |
味噌汁 | 90 | 具にかかわらず同値段 |
ふりかけご飯 | 80 or 90 | のりふりかけのみ 80円 |
dish
の値段を計算する関数 price_of_dish
は以下のように定義できる.
# let price_of_dish d =
match d with
PorkCutlet -> 350
| Soup m_and_g -> 90
| Rice f -> (match f with Shake -> 90 | Katsuo -> 90 | Nori -> 80)
;;
val price_of_dish : dish -> int = <fun>
ご飯の場合,ふりかけで値段が違うので,ふりかけを表す f
についてさらに match
で場合分けをして値段を計算している.
実は,コンストラクタパターンの引数部にさらにコンストラクタパターンを書いて入れ子にすることで,ふたつの match
をひとつにまとめることができる.
# let price_of_dish d =
match d with
PorkCutlet -> 350
| Soup m_and_g -> 90
| Rice Shake -> 90
| Rice Katsuo -> 90
| Rice Nori -> 80
;;
val price_of_dish : dish -> int = <fun>
Rice Katsuo
は d
が Rice Katsuo
である場合を表すパターンとなっている.さらに,orパターンを使うと,鮭と鰹の場合をまとめることができる.
# let price_of_dish d =
match d with
PorkCutlet -> 350
| Soup m_and_g -> 90
| Rice (Shake | Katsuo) -> 90
| Rice Nori -> 80
;;
val price_of_dish : dish -> int = <fun>
味噌汁も同じ値段なので,さらに or パターンでまとめて書いてもよいが,この場合は,Soup _ | Rice (Shake | Katsuo) -> 90
のように,Soup
の味噌,具材に名前をつけないようにしないといけない.(これは,orパターンで列挙した可能性のうちどの場合だったとしても—Soup
だろうが Rice
だろうが—->
の右側で同じ変数が使えなければならない,ということからきている.Rice Shake
や Rice Katsuo
の場合は新しい変数が導入されていないので,Soup
もそれにあわせているわけである.)個人的には値段改定があってどちらかだけ値上りした時に少し面倒なので,このあたりに留めたいと思っている.
このように場合の数が増えて,パターンが複雑化してくると exhaustiveness check のありがたみが段々とわかってくる8.
isVeggieDish : dish -> bool
を定義せよ.(解答9)
次に,一品料理を並べて定食(menu
型)を作ろう.定食は一品料理を好きな数だけ並べてよいものとする.好きな数だけ並べたいので,1皿の場合,2皿の場合,と皿の数に対応したコンストラクタを用意するのでは間に合わない.
そこで,定食を,
Smile
10Add
という二種類のコンストラクタで表現する.
Add
の of
以下に今定義しようとしている型の名前 menu
が現れている再帰的な型定義となっている.しかし,ここまでのことが理解できていれば menu
型の値を作るのはそれほど難しくはない.
# let m1 = Smile;; (* 文無し定食 *)
val m1 : menu = Smile
# let m2 = Add {d = PorkCutlet; next= m1};; (* トンカツのみ *)
val m2 : menu = Add {d = PorkCutlet; next = Smile}
# let m3 = Add {d = Rice Nori; next= m2};; (* のりふりかけご飯を追加 *)
val m3 : menu = Add {d = Rice Nori; next = Add {d = PorkCutlet; next = Smile}}
# let m4 = Add {d = Rice Shake; next= m3};; (* ごはんのおかわりつき *)
val m4 : menu =
Add
{d = Rice Shake;
next = Add {d = Rice Nori; next = Add {d = PorkCutlet; next = Smile}}}
さて,定食の値段を計算する関数 price_of_menu
を考えてみよう.引数は menu
なので自然に以下のような定義が思いつく.
(Add
の引数はレコードなのでレコードパターンを使って,フィールドの値を let
を使わずに取り出していることにも注目してもらいたい.)
さて,???
の部分はどうなればよいだろうか.要するに1品目(d1
)と残り(m'
)から構成されている定食なのだから,この値段はd1
の値段と残りm'
の値段である.残りの値段は price_of_menu
を 再帰的に 呼出すことによって計算できる.以下が完成版の定義である(rec
を忘れずに).
# let rec price_of_menu m =
match m with
Smile -> 0
| Add {d = d1; next = m'} -> price_of_dish d1 + price_of_menu m'
;;
val price_of_menu : menu -> int = <fun>
データ構造が再帰的に定義されているので,それを処理する関数は自然と再帰関数になる.
isVeggieMenu : menu -> bool
を定義せよ.(解答11)
2019年度用に改訂予定
さて,(ここまでの)レコードについては,レコードを一度作ったら,そのフィールドの値を変更することはできない.{〈式0〉 with 〈フィールド名1〉 = 〈式1〉; ... ; 〈フィールド名n〉 = 〈式n〉}
という形式で,既存のレコード〈式0〉を使って,フィールドの新しい値が with
以下に従う(指定されていないフィールドは〈式0〉の値を再利用)ような新しいレコードを作ることはできる(これを functional update ということがある)が,これはあくまでも新しいレコードを作るもので,〈式0〉のレコードはそのままである.
# let p = {origin with y = 3};;
val p : point = {x = 0; y = 3}
# origin;;
- : point = {x = 0; y = 0}
変更可能レコード(mutable record)は,フィールド値を(破壊的に)変更できるようなレコードのことである.変更が可能かどうかはレコード型の宣言を行う際に,フィールド名の前に mutable
をつけることでフィールド毎に指定することができる.
# type mutable_point = {mutable x : int; mutable y : int; };;
type mutable_point = {mutable x : int; mutable y : int; }
# let m_origin = {x = 0; y = 0};;
val m_origin : mutable_point = {x = 0; y = 0}
フィールドの値を変更するためには 〈式1〉.〈フィールド名〉<-〈式2〉
という形式を使う.これは〈式1〉のレコードの〈フィールド名〉のフィールドの値を〈式2〉に書き換える,というものである.
変更を行った後で m_origin
の値を見ると変わっているのがわかる.このようなレコードの更新を functional update に対し,destructive update, in-place update ということがある.
このフィールド値の変更を行っている m_origin.x <- 2
は Java であれば(代入)文相当のものであるが,OCaml ではこれも式の一種である.式の型は unit
型と呼ばれ,その型の値は ()
という定数ひとつである.その他,画面に文字列や数を端末画面に表示する関数も unit
を返す関数として定義されている.unit
型は Java (や C)の void
型と似ているが,void
型の値は存在しないという点が違う12.
表示されている 20
は式の値ではなく,print_int
の呼出しの結果表示されたものであることに注意せよ.
unit
型の式は(その計算が止まれば)値が常に ()
なので,値自体にはほとんど意味がなく,その式の計算中に引き起こされるレコードのフィールドの変更や画面への文字の表示などの方が重要である.こういった,式の計算中に起こる,「値が求まる以外のこと」一般を総称して計算効果(computational effect)という.
変更可能なフィールドを持つレコードは,Java オブジェクトのように,実体はヒープ領域にあって値としてはそのレコードのID・背番号が使われていると考えるのがよい.
のようにレコードを作る際には,
x
フィールドに \(0\),y
フィールドに \(1\) を格納したレコードを作り,p1
という名前をつける. という手順が踏まれていると考える.という変更式は,p1
が指すレコードの x
フィールドに 2
を書き込む,と考えられる.
のような定義は,単に p1
という名前がついたレコードのIDに p1
という別名をつけていると考える.(実は,mutable
フィールドを持たないレコードも実装上はヒープ領域に確保していて,let
で名前がつけられているのはレコードそのものではなくレコードのIDなのだが,フィールドの書き換えが起きない限り,p1
はレコードそのものにつけられた名前で let p2 = p1;;
がレコードのコピーを行っているように考えても差し支えない.)
さて,p1
も p2
も同じレコードを指しているので,
としてから,p1.y
の値を求めると 3
が得られることになる.
一方,
のような with
を使った functional update の場合は,
x
フィールドに \(2\),y
フィールドには p2.y
を格納したレコードを作り,p3
という名前をつける. という手順が踏まれていると考える.新しいレコードをヒープ領域に確保し,(更新されたフィールド以外の)各フィールドの値はコピーしている,というのがポイントである.OCaml では Java などの代入可能な「変数」—OCamlでは 参照(reference) と呼ぶ—を mutable record の一種で表現することができる.参照は内部的には contents
というフィールドをひとつだけ持つ mutable record だが,通常は,ref 〈式〉
という構文で作る.
型は int ref
となっているが,これは「整数への参照型」である.実は,任意の型についての参照を作ることできる.
参照の型は 〈型〉 ref
と複合的な型表現になっている.ref
は単体では型ではなく,「何を格納しているか」の情報をつけてはじめて型になる.このような型を パラメータ付き型(parameterized type) と呼ぶことがある.パラメータ付き型は参照特有のものではなく,プログラマが定義するデータ型でも作ることができる.これについては多相性についての章で学ぶ.
参照は mutable record なので,.contents
や <-
で読み書きすることができるが,参照のために特別な構文が用意されている. 参照の中身の読み出しには前置演算子 !
, 更新には :=
という中置演算子を使う.
# x := 2;; (* x.contents <- 2 と同じ *)
- : unit = ()
# !x;; (* x.contents と同じ / boolean の否定ではない *)
- : int = 2
# x := !x + 1;;
- : unit = ()
# !x;;
- : int = 3
これらは Java で書くならば,
と同じようなものだが,参照の内容の読み出しには明示的に !
が必要である.これは面倒なように思われるが13変数という値を格納する箱と,その中身を区別する,という意味で,概念の整理に役に立つと思う14.
式をセミコロンで区切って並べると,左から順に逐次実行(sequential execution)をして,最後の式の値が全体の値となる.
Java ではセミコロンは文や宣言の 終端記号(terminator) なので各文・宣言の後に必ずセミコロンが必要になるが,OCaml では式と式の間にはさむ 区切り記号(separator) なので,最後の式の後にはセミコロンをつけてはいけない.
逐次実行において最後以外の式の値は単に捨てられてしまう.ので,unit
以外の型の式を書くと,「捨てていいの?」という意味で警告が発生する.また,計算効果がない式を書くのは,計算をして結果を捨てるだけなので無駄である.
# 1+1; 5;;
Characters 0-3:
1+1; 5;;
^^^
Warning 10: this expression should have type unit.
- : int = 5
OCaml には ignore
という引数に関わらず ()
を返すだけの関数があり,値を捨てたい場合は,この関数を呼んで引数を捨てることを明示する慣習となっている.
OCaml での,if
式 if 〈条件式〉 then 〈式1〉 else 〈式2〉
は「〈条件式〉
の値が true
ならば 〈式1〉
の値を,false
ならば〈式2〉
の値になる」というものだが,〈式1〉
や〈式2〉
が計算効果を持つ場合は,JavaやCなどのif
文に近い役割になる.
# let is_positive n =
if n > 0 then print_string "n is positive\n"
else print_string "n is not positive\n";;
# is_positive 100;;
n is positive
- : unit = ()
Java の if
文では else
部分を省略できるが,OCamlのif
式も unit
型の式に限って else
を省略することができる.
# let is_positive' n =
if n > 0 then print_string "n is positive\n";;
# is_positive' 100;;
n is positive
- : unit = ()
# is_positive' (-100);;
- : unit = ()
then
部が unit
型の式でない場合にはエラーになる.これは,条件が成立しなかった場合に,どんな値になればよいかわからないためであるとも考えられるし,if e0 then e1
は if e0 then e1 else ()
の略記だと考えてもよいだろう.
let f n = if n > 0 then 2;;
Characters 24-25:
let f n = if n > 0 then 2;;
^
Error: This expression has type int but an expression was expected of type
unit
if
と逐次実行の ;
の優先度には注意が必要である.if
の方が ;
よりも強く結合するので,then
と else
の間にうかつに ;
を入れると,妙なエラーに遭遇することになる.
# let is_positive n =
if n > 0 then print_int n; print_string " is positive"
else print_int n; print_string " isn't positive";;
この定義の意図は,n
の値に続けて "is positive" もしくは "isn't positive" を表示させるというものだが,OCaml は ;
が現れたところで一旦 if
を切ってしまうので, else
が現れたところで,突然 else
を書くんじゃない,と文法エラーになってしまう.
これを修正するには,;
でつながれた部分を begin
, end
で囲む15.
# let is_positive n =
if n > 0 then
begin
print_int n;
print_string " is positive"
end
else print_int n; print_string " isn't positive";;
val is_positive : int -> unit = <fun>
しかし,else
にある;
についても,;
が現れたところで,if
が終わったと判断するので,これではn
の値に関わらず print_string " isn't positive"
が実行されてしまい
という結果になってしまう.正しくは,
# let is_positive n =
if n > 0 then
begin
print_int n;
print_string " is positive"
end
else
begin
print_int n;
print_string " isn't positive"
end;;
である.
この優先順位のつけ方は,奇妙に感じるかもしれないが,Java や C言語の「then
や else
部に複数の文を書く時には {}
で囲まければいけない」という規則に合わせているような気もする.
また,begin
, end
は,実は (
,)
と同じで,何でも囲むことができるし,begin
, end
の代わりに (
, )
を使ってもよい.
# begin 2 + 3 end * 10;;
- : int = 50
# let is_positive n =
if n > 0 then (
print_int n;
print_string " is positive"
) else (
print_int n;
print_string " isn't positive"
);;
val is_positive : int -> unit = <fun>
ふつうの数式を begin
end
で囲ってもよい(ふつうはしないが).
ただし,さすがに begin
を )
で閉じたり, (
を end
で閉じたりしてはいけない.
# begin 2 + 3 ) * 10;;
Characters 0-5:
begin 2 + 3 ) * 10;;
^^^^^
Syntax error: 'end' expected, the highlighted 'begin' might be unmatched
# (2 + 3 end * 10;;
Characters 0-1:
(2 + 3 end * 10;;
^
Syntax error: ')' expected, the highlighted '(' might be unmatched
OCaml にもループのための構文 while
と for
が用意されている.が,使用頻度が少ないせいか,特に for
は Java や C のそれと比べてかなり制限されている.
while 〈条件式〉 do 〈ループ本体式〉 done
… 〈条件式〉
が true
である限り 〈ループ本体式〉
を評価する.for 〈変数〉 = 〈初期値〉 to 〈上限〉 do 〈ループ本体式〉 done
… 〈変数〉
を 〈初期値〉
(整数)から 1 ずつ増やし,〈上限〉
以下である限り 〈ループ本体式〉
を評価する.for 〈変数〉 = 〈初期値〉 downto 〈下限〉 do 〈ループ本体式〉 done
… 〈変数〉
を 〈初期値〉
(整数)から 1 ずつ減らし,〈下限〉
以上である限り 〈ループ本体式〉
を評価する.いずれの構文でも 〈ループ本体式〉
は unit
型であることが要請される.
[ 前の資料 | 講義ホームページ・トップ | 次の資料 ]
Java や C のように,(プログラムが書かれた)ファイルをコンパイルするバッチコンパイラもある.↩︎
比較演算子として ==
や !=
もあるのだが,ここでは説明しない.↩︎
解答例
let rec gcd(n, m) =
if n < m then gcd(m, n)
else
let n' = n mod m in
if n' = 0 then m else gcd(m, n')
組(tuple)と呼ばれる,要素を並べて作るデータについては基本的なものだが省略する.↩︎
解答例
let sum r1 r2 =
let r3_num = r1.num * r2.den + r2.num * r1.den in
let r3_den = r1.den * r2.den in
let g = gcd(r3_num, r3_den) in
{num=r3_num / g; den=r3_den / g}
つまり,bool
型は,true
と false
という(英小文字で始まる特別な)コンストラクタから成る列挙型である.↩︎
複数の情報を持たせたい場合,レコードではなく(記述量が少ない)組を使うことが多いのだが,Java との対応関係が見て取りやすいレコードを使っている.↩︎
とはいえ,場合によっては,以下のようにふりかけの値段を計算する price_of_rice
を定義し,それを使うことがベターなこともありえる.この定義が最善,といっているわけではない.
# let price_of_furikake f =
match f with
Shake -> 15
| Katsuo -> 15
| Nori -> 5
;;
val price_of_furikake : furikake -> int = <fun>
# let price_of_dish d =
match d with
PorkCutlet -> 350
| Soup m_and_g -> 90
| Rice f -> price_of_furikake f + 75
;;
val price_of_dish : dish -> int = <fun>
解答例
↩︎解答例
let rec isVeggieMenu m =
match m with
Smile -> true
| Add {d = d1; next = m'} -> isVeggieDish d1 && isVeggieMenu m'
Java で返値型が void
になっているメソッドを書いていると,この型の要素なんてないのに return;
は一体何を返しているんだろう,という不思議な気持ちになります.↩︎
実際面倒である↩︎
実際,Java や C で変数の説明には,変数のアドレスを表す 左辺値(L-value) と変数の中身を表す 右辺値(R-value) という概念が使われる.つまり x = x + 1;
という文は,「x
の左辺値のアドレスに x
の右辺値プラス1を格納する」というように説明される.↩︎
Rubyっぽい?(歴史的には順序が逆ですが.)↩︎
Copyright 五十嵐 淳, 2016, 2017, 2018, 2019, 2020