2016年度「プログラミング言語」配布資料 (4)

五十嵐 淳 (京都大学大学院情報学研究科 通信情報システム専攻)

2016年10月31日

[ 前の資料 | 講義ホームページ・トップ | 次の資料 ]

under construction2017年版に向け改訂中

OCaml とは

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の概要をプログラム例を混じえて紹介する.二分探索木がプログラムできるための最短ルートを通るため,トップページで紹介している他の資料にもあたってもらいたい.

OCaml は電卓として使える

OCaml には入力を即座に実行して結果を表示する対話的処理系が用意されていて,インタラクティブにプログラムの実行を進めることができる.1 OCamlでは入力された式を値に評価することでプログラムの実行が進むので,対話的処理系は電卓のようなものである.

# 1 + 2 * 3;;
- : int = 7

# が処理系による入力プロンプトの記号で以降が実際の入力である.;; は電卓の=キーのようなもので,式の入力の終わりを告げるための記号である.次の行に入力された数式の計算結果(値と呼ぶ) 7 が表示されている.int は式の型を示している.

# 5 < 8;;
- : bool = true
# 5 = 2 || 9 > -5;;
- : bool = true

このように比較演算の結果は true, false というbool型の定数で表される.等号は == ではなく = でよい.また,不等号は <> である.2 基本的なデータとしては,他にも浮動小数点数の float 型,文字列のstring型などがあるがここでは紹介しない.

OCaml はメモリ機能がついた電卓として使える

OCaml ではlet宣言を使って式の計算結果に名前をつけることができる.OCaml ではこの名前のことを変数と呼ぶ.

# let x = 1 + 2 * 3;;
val x : int = 7
# x - 10;;
- : int = -3

val は値を表す名前が宣言されたことを表している.(型を定義すれば val ではなく type になったりする.)

これは Java であれば,変数宣言(と初期化)

int x = 1 + 2 * 3;

に相当すると考えられるが,いくつか違いがある.

  • 式の計算結果がまずあって,それに名前を付けるので,int x; のように変数の宣言だけをする,ということはできない.
  • 変数の型を指定する必要はなく,処理系が勝手に右辺を見て変数の型を推論してくれる.これはMLの大きな特徴のひとつである 型推論 である.プログラマが型を指定したければ変数の後に : 〈型〉 を書く.
# let x : int = 1 + 2 * 3;;
val x : int = 7
  • 変数への(再)代入はできない.ただし,同じ名前を再利用することはできる.
# let x = 4 < 5;;
val x : bool = true

「これは代入ではないのか?」と思うかもしれない.それに対する説明はもうすぐ行う.

OCaml は関数電卓として使える

OCaml の主要なプログラム構成要素は関数である.関数も let を使って宣言する.関数の定義の構文は,

let 〈関数名〉(〈パラメータ1〉, ..., 〈パラメータn〉) = 〈式〉

となる.〈パラメータ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>である,何らかの関数であることを示している.

パラメータの数がひとつの時は括弧を省略することができ,そして,ふつうは省略する(ただし,関数名との間には空白が必要である).

# let double n = n + n;;
val double : int -> int = <fun>
# double 256;;
- : int = 512

条件分岐

条件によって異なる値を計算したい時にはif式を使う.if式は

if 〈条件〉 then 〈式1else 〈式2

という形で,〈条件〉(これは 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」と解釈してしまい,型についてのエラーが出てくる.)

再帰関数

再帰関数を宣言する時には,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 と空白をなくして書いても無駄である.)

fact0 (や負数)を与えた場合は,この計算は

   fact 0
→ 0 * fact (-1)
→ 0 * (-1) * fact (-2)
→

となって止まらず,ついには(式が大きくなり過ぎて)メモリが足りなくなって overflow エラーになってしまう.

# fact 0;;
Stack overflow during evaluation (looping recursion?).

変数の有効範囲と静的スコーピング

関数の定義中で使ってよい変数はパラメータだけではない.それまでに宣言された変数を使うことができる.以下は,与えられた半径から円の面積を計算する関数 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 が使われている.

さて,上で,同じ変数名の定義が複数あってもそれは代入ではない,といったが,それは以下のような例から確認できる.

# let pi = 3.00;;
val pi : float = 3.
# circle_area 4.0;;
- : float = 50.24

関数定義中のpiの使用は,本体を計算する時点での pi の値をとってくるのではなく,関数が定義された時点での pi の値を使うことになる.一方で,以下のような計算をすると再定義された pi の値 3.0 が引数になるし,

# circle_area pi;;
- : float = float = 28.259999999999998

(新しい pi を)関数定義で使えば,円周率が 3.0 となる面積計算になる.

# let circle_area2 radius = pi *. radius *. radius;;
val circle_area2 : float -> float = <fun>
# circle_area2 4.0;;
- : float = 48.

このように OCaml では,変数の使用とそれに対応する定義は,使用箇所から上に辿って一番近いものになり,変数を使った計算が起こる瞬間にその名前の値が何であるか,とは関係がない.このような,プログラムテキスト中での字面上の関係を使って使用と定義の対応を決める方式を,静的スコーピング(static scoping),という.

let式による局所定義

let を式の中で使うことによって,式の計算中,値に一時的に(局所的に)名前をつけることができる.これが「let 式」である.let式の構文は

let 〈変数〉 = 〈式1in 〈式2

であり,直観的には「〈式2〉の計算中,〈変数〉を〈式1〉(の値)とする」という意味で,〈式1〉を計算し,その値に〈変数〉という名前をつけ,〈式2〉の値を計算する.

# let n = 5 * 2 in n + n;;
- : int = 20

この〈変数〉の有効範囲は〈式2〉だけで,〈式2〉の外側では使用することができない.

# n;;
Characters 0-1:
  n;;
  ^
Error: Unbound value n

let式もただの式の一種なので,式が書けるところでは自由に使うことができる.典型的には,〈式2〉にletを使うことで複数の変数を順に定義していくように書ける.

# let x1 = 1 + 1 in
  let x2 = x1 + x1 in
  let x3 = x2 + x2 in
  x3 + x3;;
- : int = 16

また,以下のように,関数定義の本体で 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 プログラムの実行

OCaml プログラムの実行過程を説明するのはそんなに難しくない.はっきりいって Java より簡単である.ここでもフレームの概念を使う.フレームは変数とその値を関連づけたものであり,式の計算にあたっては常に何らかのフレームのもとで行い,式に現れる変数の値はそのフレームから取ってくる.この変数・値の関連づけを変数束縛ともいう.以下,構文毎に実行がどう行われるかを説明する.

let宣言 (let\(x\)=\(e\);;)
現在のフレームを使って式\(e\)の値 \(v\) を求める.現在のフレームに,\(x\) = \(v\) という変数束縛を付加したものが次の入力を処理する時のフレームとなる.なお,フレームに同じ名前の変数が既に現れている場合でも,古い関連づけはそのままで新しい関連づけを追加する.
let式 (let\(x\)=\(e_1\)in\(e_2\))
現在のフレームを使って式 \(e_1\) の値 \(v_1\) を求める.現在のフレームに変数束縛 \(x\) = \(v\) を追加したフレームを使って,\(e_2\) の値 \(v_2\) を求める.\(v_2\) がlet式の値となる.拡張したフレームは \(v_2\) を求めるため(だけ)には使うが,その後は特に使わないことに注意せよ.
変数式 (\(x\))
変数が式の中で使用されている時は,現在のフレームからその変数の束縛を探して値を取り出す.この際,探索は新しい束縛から順に行っていく.そのため,同じ変数を何度も宣言した場合は,一番最後のものを使うことになる.
関数呼出し式 (\(f\)(\(e_1\),\(\ldots\),\(e_n\)))
まず,現在のフレームを使って \(e_1, \ldots, e_n\) の値 \(v_1, \ldots, v_n\) を求める.(この値を求める順番は,実は特に指定されておらず実装依存である.3) 次に関数 \(f\) の本体式を,\(f\) が定義された時点でのフレームに束縛 \(x_1 = v_1, \ldots, x_n = v_n\) を追加したようなフレームで評価する(\(x_i\)\(f\)\(i\)番目のパラメータ変数).その値が関数呼出し式の値となる.\(f\) が定義された時点でのフレームを使う,というのがポイントである.これによって静的スコーピングが実現できる.

データ構造

OCaml では,type宣言を使って様々な複合的なデータ型を定義することができる.4

レコード

レコード(record)は,いくつかの値を(それぞれに名前をつけて)まとめたような値である.例えば二次元座標(座標は整数値)を表すレコード型 point は以下のように定義できる.

# type point = {x: int; y: int};;
type point = { x : int; y : int; }

x, y をレコードのフィールド(field)名と呼ぶ.レコード型の値を作るには,

# let origin = {x = 0; y = 0};;
val origin : point = {x = 0; y = 0}

などとする.(区切りはセミコロン ; なので注意してもらいたい.)

レコードから各フィールドの値を取り出すには,ふたつの方法がある.ひとつはドット記法と呼ばれるものを用いるものである.

# 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などの変数を宣言する機構と組み合わせて使うことができる.

例:

# let {x = ox; y = oy} = origin;;
val ox : int = 0
val oy : int = 0

{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 座標を取り出す関数は

# let get_x {x} = x;;

のように書ける.

ヴァリアント

ヴァリアント(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 では小文字でもよい.)

こういう定数の(有限)集合を定数を列挙することで構成されるデータ型を列挙(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式は

match 〈式0with 〈パターン1〉 -> 〈式1〉 | ... |  〈パターンn〉 -> 〈式n〉

という構文で,〈式0〉の値を〈パターン1〉から順に照合していき,i番目でマッチしたら(形があったら),〈式i〉を計算しその値がmatch式全体の値となる.if式は実は

match 〈式0with true -> 〈式1〉 | false -> 〈式2

と書ける.今回の例では〈パターンi〉は単にコンストラクタの名前となっているが,もっと複雑なヴァリアントの場合には,パターンも複雑なものがでてくる.match式には,パターンのexhaustiveness check といって,〈パターン1〉から〈パターンn〉が〈式0〉の値としてありうる全ての場合を尽くしているかをチェックしてくれる機能がある.上の isVeggief の型は 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>

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や,この例のようにレコードでもよい5),各コンストラクタに何の情報が付与されるかを指定する.具体的な一品料理は,コンストラクタの後に(関数呼出しの引数のように)付帯情報を書く.

PorkCutlet;;
- : dish = PorkCutlet
# Soup {m = Aka; g = Tofu};;
- : dish = Soup {m = Aka; g = Tofu};;
# Rice Shake;;
- : dish = Rice Shake

引数を取るコンストラクタを単体で使うことはできない.

Rice;;
Characters 0-4:
  Rice;;
  ^^^^
Error: The constructor Rice expects 1 argument(s),
       but is applied here to 0 argument(s)

与えられた dish が固形かどうかを判定する関数 isSolidisVeggie と同じように match を使って記述するが,引数を取るコンストラクタについては引数につける名前を同時に指定する.

# let isSolid d =
    match d with
      PorkCutlet -> true
    | Soup m_and_g -> false
    | Rice f -> true
    ;;
val isSolid : dish -> bool = <fun>

この例では f などの付帯情報を表す変数は全く使っていないが,対応する(->の右側の)式で使うことができる.以下は料理の値段を計算する関数である(ふりかけの種類によって微妙にごはんの値段が違う).

# 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>

実は,コンストラクタパターンの引数部にさらにコンストラクタパターンを書いて入れ子にすることで,ふたつの 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 KatsuodRice Katsuo の場合を表すパターンとなっている.このように場合の数が増えてくると exhaustiveness check のありがたみが段々とわかってくる.6

再帰ヴァリアント

次に,一品料理を並べて定食(menu型)を作ろう.定食は一品料理を好きな数だけ並べてよいものとする.好きな数だけ並べたいので,1皿の場合,2皿の場合,と皿の数に対応したコンストラクタを用意するのでは間に合わない.

そこで,定食を,

  • 0皿の定食を表す Smile 7
  • 既存の定食に,もう一皿先頭に加えたコース料理を表す Add

という二種類のコンストラクタで表現する.

type menu = Smile | Add of {d: dish; next: menu};;

Addof 以下に今定義しようとしている型の名前 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: m1};;
val m3 : menu = Add {d = Rice Nori; next = Add {d = PorkCutlet; next = Smile}}
# let m4 = Add {d = Rice Shake; next: m1};;
val m4 : menu =
  Add
   {d = Rice Shake;
    next = Add {d = Rice Nori; next = Add {d = PorkCutlet; next = Smile}}}

さて,定食の値段を計算する関数 price_of_menu を考えてみよう.引数は menu なので自然に以下のような定義が思いつく.

# let price_of_menu m =
    match m with 
      Smile -> 0
    | Add {d = d1; next = m'} -> ???
    ;;

(Add の引数はレコードなのでレコードパターンを使って,フィールドの値を let を使わずに取り出していることにも注目してもらいたい.)

さて,??? の部分はどうなればよいだろうか.要するに1品追加したのだから,1品目(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>

データ構造が再帰的に定義されているので,それを処理する関数は自然と再帰関数になる.

データ構造と引数渡しについて

Java において,オブジェクトのデータは全てヒープ領域と呼ばれるメモリ領域に割り当てられ,変数にはオブジェクトそのものが格納される,というより,オブジェクトへの参照/ポインタ/IDが格納されているのであった.つまり,

BinarySearchTree t = new Branch(new Leaf(), 3, new Leaf());

などとやれば,3つのオブジェクトがヒープ領域に作られ,t にはBranch オブジェクトのID(への参照)が,Branch オブジェクトのインスタンス変数 left, value, right にはそれぞれLeafオブジェクトその1への参照,3,Leafオブジェクトその2への参照が格納されているのであった.

OCaml のレコードは,データを集めてひとつのまとまりとして扱えるようにする,という点で Java のオブジェクトに似ているわけだが,

let r1 = {x = 0; y = 0};;

というようにレコードに let で名前をつけた時,それはレコードに名前を付けているのであろうか,それともレコードにはそれぞれ ID なるものがあって,それに r1 という名前を付けているのであろうか.また,上の m3, m4 のようにレコードのフィールドに入れ子状にレコードが現れる時,next フィールドには何が入っているのだろうか.

その答えは,

プログラムの挙動を考える上ではレコードそのものに r という名前をつけている・(コンストラクタ付の)レコードが next フィールドに入っていると思ってよい

である.「プログラムの挙動を考える上では」と書いてあるのは,実装がどうなっているかはまた別の話,という意味でもある.レコードは r の束縛を含むフレームの中にあるかもしれないし,Java のようにレコードはヒープ領域にあって,r にはレコードのIDが格納されているのかもしれないが,プログラムからそれを伺い知ることはできないので気にしなくてもよい.また,

let r2 = r1;;

などとした時にも,r2r1 がレコード {x = 0; y = 0} に束縛されている,ということが大事なのであって,メモリ上でひとつのレコードを共有しているのか,ふたつの別のレコードがあってそれぞれ別のレコードを意味しているのかは,ほとんどの場合気にする必要がない.それは,レコードを引数として関数に渡した時も同じである.

レコードの等しさも = で判定することができる.この際の判定は,各フィールドの値が等しいかで判定される.これは ID を比較する Java の == とは異なるので注意したい.実際,

let r3 = {x = 0; y = 0};;

とした上で,r1 = r2r1 = r3 の値を比べてみよ.どちらも true である.
一方,Java では

BinarySearchTree t1 = new Branch(new Leaf(), 3, new Leaf());
BinarySerachTree t2 = t1;
BinarySearchTree t3 = new Branch(new Leaf(), 3, new Leaf());

とした時に t1 == t2true だが t1 == t3false となる.8

変更可能レコード

さて,(ここまでの)レコードについては,レコードを一度作ったら,そのフィールドの値を変更することはできない.{〈式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.x <- 2;;
- : unit = ()
# m_origin;;
- : mutable_point = {x = 2; y = 0}

変更を行った後で m_origin の値を見ると変わっているのがわかる.このようなレコードの更新を functional update に対し,destructive update, in-place update ということがある.

このフィールド値の変更を行っている m_origin.x <- 2 は Java であれば(代入)文相当のものであるが,OCaml ではこれも式の一種である.式の型は unit 型と呼ばれ,その型の値は () という定数ひとつである.その他,画面に文字列や数を端末画面に表示する関数も unit を返す関数として定義されている.unit 型は Java (や C)の void 型と似ているが,void 型の値は存在しないという点が違う.9

# print_int;;
- : int -> unit = <fun>
# print_int (5+15);;
20- : unit = ()

表示されている 20 は式の値ではなく,print_int の呼出しの結果表示されたものであることに注意せよ.

unit 型の式は(その計算が止まれば)値が常に () なので,値自体にはほとんど意味がなく,その式の計算中に引き起こされるレコードのフィールドの変更や画面への文字の表示などの方が重要である.こういった,式の計算中に起こる,「値が求まる以外のこと」一般を総称して計算効果(computational effect)という.

変更可能レコードを理解する

レコードの変更可能なフィールドは,Java の(インスタンス)変数のように考えると理解できる.すなわち,フィールドのデータはヒープ領域にあって,フィールドには,そのデータのID・背番号が格納されている,と考えるのである.

# let p1 = {x = 0; y = 1};;
val p1 : mutable_point = {x = 0; y = 1}

のようにレコードを作る際には,

  1. 整数値を格納する領域をヒープに確保し 0 を書き込む.その領域へのポインタを (l_1) とする.
  2. 整数値を格納する領域をヒープに確保し 1 を書き込む.その領域へのポインタを (l_2) とする.
  3. x フィールドに (l_1),y フィールドに (l_2) を格納したレコードを作り,それに p1 という名前をつける.
    という手順が踏まれていると考える.
# p1.x <- 2;;
- : unit = ()

という変更式は,フレーム中で p1 という名前がついたレコードの x フィールドが指す領域に 2 を書き込む,と考えられる.

{.ocaml} # let p2 = p1;; val p2 : mutable_point = {x = 2; y = 1}
のような定義は,レコードのコピーを作って p2 という名前をつけていると考えていたわけだが,mutable なフィールドについてはポインタだけをコピーしている.すると,p1p2y フィールドの指す先は同一領域なので,

# p2.y <- 3;;
- : unit = ()

としてから,p1.y の値を求めると 3 が得られることになる.

逐次実行

式をセミコロンで区切って並べると,左から順に逐次実行(sequential execution)をして,最後の式の値が全体の値となる.

# p1.x <- 0; p2.y <- 4; print_int p1.y; 100
4- : int = 100

最後以外の式の値は単に捨てられてしまう.ので,unit以外の型の式を書くと,「捨てていいの?」という意味で警告が発生する.また,計算効果がない式を書くのは,計算をして結果を捨てるだけなので無駄である.

# 1+1; 5;;
Characters 0-3:
  1+1; 5;;
  ^^^
Warning 10: this expression should have type unit.
- : int = 5

[ 前の資料 | 講義ホームページ・トップ | 次の資料 ]


  1. Java や C のように,(プログラムが書かれた)ファイルをコンパイルするバッチコンパイラもある.

  2. 比較演算子として ==!= もあるのだが,ここでは説明しない.

  3. といっても,実装がひとつしかないのだが…その唯一の実装では,今のところ,右から左の順で計算される.(が,その順番が将来にわたって保証されているわけではない.)

  4. 組(tuple)と呼ばれる,要素を並べて作るデータについては基本的なものだが省略する.

  5. 複数の情報を持たせたい場合,レコードではなく(記述量が少ない)組を使うことが多いのだが,Java との対応関係が見て取りやすいレコードを使っている.

  6. とはいえ,場合によっては,以下のようにふりかけの値段を計算する 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>
  7. http://toyokeizai.net/articles/-/70541

  8. 実は OCaml にも == という演算子があり,ID をベースとした比較をすることができる.ただしIDの割り当て方に Java のように厳密な決まりが(言語仕様上)なく,t1 == t2t1 == t3 の値がどうなるかは実装依存,ということになっている.(ので,あまり使うべきではないのでが,OCaml の実装はひとつしかないし,バージョンを越えて == の動作は安定しているので,使うこともよくある.

  9. Java で返値型が void になっているメソッドを書いていると,空集合の要素なんてないのに return; は一体何を返しているんだろう,という不思議な気持ちになります.


Copyright 五十嵐 淳, 2016, 2017