Learn OCaml

Author

gentam

Status
draft
Date
2017-11-08
Category

learning

Tag

ocaml, language

Updated

2017-11-19

コンピュータシステムの理論と実装の6章 で アセンブラを実装する練習があったので,OCamlで書いてみようと思って調べたときのメ モ.

参考にしたのは主に以下:

Contents

Intro

OCaml (オーキャムル)はINRIA(フランス国立情報学自動制御研究所)で開発された. 歴史的には,エディンバラ大学で1970年代に開発されたML (Meta Language)の流れを組む 言語で,Caml (Categorical Abstract Machine Language) という名前だったが,後にオ ブジェクト指向の機能が追加され,1996にObject Camlとして再リリースされた.

主な特徴:

OCamlで書かれた有名なアプリケーションとして,Coqなどがある. OCamlを書く人のことを岡村というらしい.

環境構築

MacでHomebrewユーザなら brew install ocaml で入る. パッケージマネージャの OPAM も同時に brew install opam で入れておくと良さそう.

主なコマンド:

  • ocaml REPLに入る.トップレベルと呼ぶ

  • ocamlc: コンパイル

  • その他:

    • ocamlrun

    • ocamlopt

    • ocamldep

    • ocamldebug

    • ocamlyacc, ocamllex

    • ocamlp4

    • ocamlprof

    • ocamldoc

ocaml で入れるデフォルトのトップレベルは履歴が使えなくて不便なので, 以下の2通りで解決できる.

  1. brew install rlwrap rlwrap ocaml で履歴を有効にする

  2. opam install utoputop を代わりに使う

無駄に派手だけど,タブ補完どころかリアルタイムで候補を出してくれたりと便利なので utop を使うことにする.

Hello, world

$ cat > hello.ml
print_string "hello world"
$ ocamlc hello.ml -o hello && ./hello
hello world

基礎

コメントは (* comment *) というように書く.ネストしたり行をまたいでも大丈夫 . トップレベルでは, ;; によって文を区切る(その時点で評価する)

OCaml type

Range

int

31-bit signed int (roughly +/- 1 billion) on 32-bit processors, or 63-bit signed int on 64-bit processors

float

IEEE double-precision floating point, equivalent to C's double

bool

A boolean, written either true or false

char

An 8-bit character

string

A string

unit

Written as ()

int

int の最大値は32bitのCPUで,2^30.64bitのCPUで2^62. これは,符号に1ビット,ポインタかどうかの判断に1ビットを利用するため:

# 1 + 1;;
- : int = 2
# 0 - 1;;
- : int = -1
# 2 * 3;;
- : int = 6
# 3 / 2;;
- : int = 1
# max_int;;
- : int = 4611686018427387903
# min_int;;
- : int = -4611686018427387904
# max_int + 1 = min_int;;
- : bool = true

10進法以外の表現:

# 0b10;; (* 2進数 *)
- : int = 2
# 0o10;; (* 8進数 *)
- : int = 8
# 0x10;; (* 16進数 *)
- : int = 16

float

float では, 2.997e3 のように指数表現が可能. C言語のdoubleに対応.

計算において,int と float は区別され,C言語のように暗黙に型変換が行われることは ない.そのため,float の演算子には,全て"."(ドット)が後に付き, 以下のようになる:

# 1.0 +. 2.0;;
- : float = 3.
# 1. -. 2.;;
- : float = -1.
# 4.2 /. 2.;;
- : float = 2.1
# 3. *. 2.;;
- : float = 6.
# 3 *. 2.;;
Error: This expression has type int but an expression was expected of type
         float
# 3 * 2.;;
Error: This expression has type float but an expression was expected of type
         int

floatの特別な値:

# infinity;;
- : float = infinity
# neg_infinity;;
- : float = neg_infinity
# nan;; (* not a number *)
- : float = nan
# max_float;;
- : float = 1.79769313486231571e+308
# min_float;;
- : float = 2.22507385850720138e-308

bool

論理値は,truefalse:

# true <> false;;
- : bool = true
# not true = false;;
- : bool = true
# true && false;;
- : bool = false
# true || false;;
- : bool = true
# false <> 0;;
Error: This expression has type int but an expression was expected of type
         bool
  • equality を確かめるオペレータには === がある

  • inequality を確かめるオペレータには <>!= がある

  • 詳細はマニュアル参照 Pervasivesモジュール

char

1バイトの任意のデータ.文字を表現するには '' (シングルクオート)で囲む:

# 'A';; (* char = 'A' *)
# 'ABC';; (* Error *)
# 'ABC';; (* Error *)

エスケープ
# '\n';; (* char = '\n' *)
# '\x41';; (* char = 'A' *)
# '\065';; (* char = 'A' *)

string

文字列は, "" (ダブルクオート)で囲む.

C言語のようなcharの配列ではなく専用の型. 32bit環境では,約15MB,64bit環境では2^57-9文字まで:

# "Hello" (* string = "Hello" *)
# print_string "A\tB\tC\n";;
A    B    C
- : unit = ()
"Hello" ^ "world";; (* 連結 *)
- : string = "Helloworld"
"ABC".[0];; (* indexは0スタート *)
- : char = 'A'

多量の文字列を扱うときはBuffer moduleを利用

unit

unit型は唯一の要素 () で表す. 何も返すものがない時に指定する:

# print_string "hello\n";;
hello
- : unit = ()

全ての関数は何らかの値を返さなければいけないため,IOのような 副作用が目的の関数には便利.C系のvoidのイメージ.

変数の束縛

変数の束縛(binding)には,let 変数名 = 式 または, let 変数名 = 式 in 式 を使う:

# let a = 1;;
val a : int = 1
# a + 2;;
- : int = 3
# let a = "OCaml" in
  let b = " lang" in
  a ^ b;;
- : string = "OCaml lang"
# a;;
- : int = 1 (* a = OCaml のスコープは式: a ^ b に限定されている *)

代入 ではなく 束縛 と表現するのは,OCamlのlet式が単に値にラベルをつけて いるのであって,C言語のように"メモリ上に型のサイズ分のスペースを用意してそこに 値を設定する"のとは違うため.

参照型

代入可能な変数である参照型は ref で作れる. 代入は := で行い,値は名前に ! をつけることで参照できる:

# let my_ref = ref 0;;
val my_ref : int ref = {contents = 0}
# !my_ref;;
- : int = 0
# let my_ref' = ref 100;;
val my_ref' : int ref = {contents = 100}
# !my_ref < !my_ref';;
- : bool = true
# my_ref := !my_ref;;
- : unit = ()
# !my_ref;;
- : int = 0

参照型は実際にはmutableなフィールドを持つ レコード 型である.

ポインターでのイメージ:

int a = 0;
int *my_ref = &a;
// my_ref -> a のアドレス
// *my_ref -> a の値

関数

関数は, fun 引数名 {, 引数名} -> で定義できる. 関数の型は 引数1の型 {-> 引数nの型 } -> 戻り値の型 という形で表現される. 名前を付けるには let 名前 = fun 引数 -> となるが,これと let 名前 引数 = 式 は同じ意味なので普通は後者の方が使われ, fun は無名関 数として利用される.

2つのintを足すという関数は,以下のようになる:

# fun x y -> x + y;;
- : int -> int -> int = <fun>

一見すると,引数と戻り値を分けて int, int -> int のようにした方が直感的なの ではないかと思ってしまう.しかし,全てを -> でつなげることによって引数と戻り 値の境界が柔軟になり,関数の部分適応を綺麗に示すことができる.

例えば int -> int -> intint を1つ与えたら,その戻り値は int -> int という新しい関数になる.これをカリー化(currying)と呼ぶ.

カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、 引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を 返す関数」であるような関数にすること(あるいはその関数のこと)である (Wikipedia)

# let add x y = x + y;;
val add : int -> int -> int = <fun>
# add 2 3;;
- : int = 5
# let add10 = add 10;; (* addに10だけを渡し,引数に10を足す新しい関数を作る *)
val add10 : int -> int = <fun>
# add10 5;;
- : int = 15

関数の組み合わせと部分適応で消費税を計算する(冗長な)例:

# let percent x = x /. 100.;;
val percent : float -> float = <fun>
# percent 1.;;
- : float = 0.01
# let tax rate price = price *. percent rate;;
val tax : float -> float -> float = <fun>
# let taxprice rate price = price +. tax rate price;; (* 税込価格 *)
val taxprice : float -> float -> float = <fun>
# taxprice 8. 100.;;
- : float = 108.
# let taxprice_jp = taxprice 8.;; (* カリー化して税率8%用の関数を作る *)
val taxprice_jp : float -> float = <fun>
# taxprice_jp 100.;;
- : float = 108.

なお再帰的な関数を定義する際には,関数名の前に rec キーワードをつけなければ いけない.詳細は 再帰の扱い で.

ラベル付き引数

関数に複数の引数を適応する際に,通常は左から定義されている順番に渡すが,引数が 多くなったり,間違えやすい場合がある.(例: src, dst の順番など)

これは引数にラベルをつけることで解決できる:

let 関数名 ~ラベル名[:引数] {...} = 式
    :引数 を省略した場合は,ラベル名がそのまま引数名になる

例:
# let make_name ~first:f ~middle ~last =
    f ^ " " ^ middle ^ " " ^ last;;
  val make_name : first:string -> middle:string -> last:string -> string =
    <fun>

# make_name ~middle:"Buckminster" ~last:"Fuller" ~first:"Richard";;
- : string = "Richard Buckminster Fuller"
# make_name "Buckminster" "Fuller" "Richard";;
- : string = "Buckminster Fuller Richard"

オプション引数

値を設定しなければデフォルトを使い,明示的に指定したときにはその値を使うといった 処理は,オプション引数を使えば実現できる.オプション引数はそれ以外より前に配置 しなければならず,またオプション引数だけの関数は作れない.作りたい場合は unit を 引数の最後に足す必要がある:

let 関数名 ?(引数 = 既定値) {...} = 式
使うとき: 関数名 ~引数:値 ...

例:
# let log_with_base ?(base = exp 1.0) x =
    log x /. log base;;
val log_with_base : ?base:float -> float -> float = <fun>

# log_with_base 10.;;
- : float = 2.3025850929940459
# log_with_base ~base:2. 256.;;
- : float = 8.

(* オプション引数とunit *)
# let f ?(x = 0) ?(y = 0) () = print_int (x + y);;
val f : ?x:int -> ?y:int -> unit -> unit = <fun>
# f ();;
0- : unit = ()
# f ~x:10 ~y:20 () ;;
30- : unit = ()

制御構文

条件分岐

if bool型の条件 then 式 [else 式]

例: 絶対値を求める
    # let x = -3;;
    val x : int = -3
    # if x > 0 then
        x
      else
       x * -1;;
    - : int = 3

if分全体として値を返すため,C系の三項演算子に近い.(条件) ? true : false; このため,thenとelse で必ず同じ型を返す必要がある.

また,else を省略する場合,そこの戻り値はunit型に決められるため,thenの戻り値も unit型にしなければいけないことに注意.

繰り返し

while文:

while bool型の式 do 式 done

例: 標準入力からデータを受け取って表示する無限ループ
    # while true do
        let line = read_line () in
        print_string line
      done;;
    hello
    hello^CInterrupted.

for文:

for 変数名 = int型の式 (to | downto) int型の式 do 式 done

例: 1-10までの数字を表示
    # for i = 1 to 10 do
        print_int i
      done;;
    12345678910- : unit = ()

for分の注意点:

  • カウンタ変数はint型

  • ステップ幅は指定できない

  • 脱出構文はない

  • for全体の値はunit, do doneの中身もunit

関数型言語において繰り返しは,リスト処理とか再帰で表現するのが基本だから, たぶん for とか while は副作用がある処理をn回行うというようなことだけに特化して いる.

データ構造

リスト

[ 要素 {; 要素} ] 前から順番にアクセスできる単一の型の集合:

# [];;
- : 'a list = [] (* 'a は任意の型を意味する型変数 *)
# let lst = [1; 2; 3];;
- : val lst : int list = [1; 2; 3]
# 0 :: lst;; (* 先頭に要素追加 *)
- : int list = [0; 1; 2; 3]
# lst @ [4; 5; 6];; (* 結合 *)
- : int list = [1; 2; 3; 4; 5; 6]

任意の型を表す型パラメータ('a)の詳細は 型宣言の応用 で. 例: map関数と 上の消費税を計算する例(taxprice_jp) を組み合わせてリスト処理:

# List.map taxprice_jp [100.; 1980.; 23500.];;
- : float list = [108.; 2138.4; 25380.]

配列

[| 要素 {; 要素} |] 配列はリストと以下の点が違う:

  • ランダムアクセスが高速

  • 要素の上書きが可能

  • 要素数を後から変更するのが苦手

# [||];;
- : 'a array = [||]
# let arr = [|1; 2; 3|];;
val arr : int array = [|1; 2; 3|]
# arr.(0);; (* 要素取得 *)
- : int = 1
# arr.(0) <- 4;; (* 上書き *)
- : unit = ()
# arr;;
- : int array = [|4; 2; 3|]

要素の追加は以下のようにできるが,新しい配列を作ってデータをコピーするため 効率的ではない:

# Array.append [|0|] [|1; 2; 3|];;
- : int array = [|0; 1; 2; 3|]

タプル

( 要素 , 要素 {, 要素} ) 任意の値をセットにする構造.違う型でもok:

# let a = ("Alice", 87);;
val a : string * int = ("Alice", 87)
# let b = ("Bob", 71);;
val b : string * int = ("Bob", 71)
# fst a;;
- : string = "Alice"
# snd b;;
- : int = 71

タプルのリストで,連想配列を実現できる:

# List.assoc 1002 [(1001, "Alice");
                   (1002, "Bob");
                   (1003, "Carol")];;
- : string = "Bob"

タプルの型は type 型名 = 型表現 * 型表現 {* 型表現} となる. これは,直積型という複数の型をセットにして1つの型とする抽象化を示している. ちなみに () は省略できる.

レコード

フィールド名付きのタプル.3つ以上の要素を扱いたい時に便利.

type 型の名前 = {[mutable] フィールド名 : 型表現 {; ...}} でレコード型を宣言 できる.mutable をつけたフィールドは上書きできる:

# type student = {id: int; name: string; mutable age: int};;
type student = { id : int; name : string; mutable age : int; }
# let r = {id = 1; name = "Alice"; age = 20};;
val r : student = {id = 1; name = "Alice"; age = 20}
# r.name;;
- : string = "Alice"
# r.age <- r.age + 1;;
- : unit = ()
# r.age;;
- : int = 21

またレコードの型で設定したフィールドは全て定義しないといけないが, 次のヴァリアントを使うことでからのフィールドも定義できる.

ヴァリアント

直和型とも呼ばれる.1つの型の内容を複数の選択肢に分けられる構造. Cでいうとunion.

type 型の名前 = コンストラクタ [of 型表現] {| コンストラクタ...}

トランプのカードを数字と絵札で表すヴァリアントの例:

# type card = Num of int | Jack | Queen | King;;
type card = Num of int | Jack | Queen | King
# Num 9;;
- : card = Num 9
# Jack;;
- : card = Jack

コンストラクタは大文字から始める. Numには,of int 型情報が設定されている.

option

組み込みのヴァリアントにoption型がある.

type a' option None | Some of 'a

これは以下のような状況で有用になる:

  1. 処理の結果として値が定義できない可能性がある

例: ゼロ除算に例外を起こさせない:

# let div x y = if y = 0 then None else Some (x / y);;
val div : int -> int -> int option = <fun>
# div 1 0;;
- : int option = None
# div 2 1;;
- : int option = Some 2
  1. 未定義の状態を許す型

例: 年齢フィールドは任意のレコード:

# type student = {id: int; name: string; mutable age: int; email: string option};;
type student = {
  id : int;
  name : string;
  mutable age : int;
  email : string option;
}
# let r = {id = 1; name = "Bob"; age = 20; email = None};;
val r : student = {id = 1; name = "Bob"; age = 20; email = None}
# let r = {id = 1; name = "Bob"; age = 20; email = Some "foo@bar.com"};;
val r : student =
  {id = 1; name = "Bob"; age = 20; email = Some "foo@bar.com"}

option型を使うことで,0やNULLポインタではなく明示的に値がないということを 記述できるようになる.

Haskellの Maybe a = Nothing | Just a と似てるかも.

パターンマッチ

受け取ったリストやタプル,ヴァリアントを分解して場合分けできる構文. 強力なswitch文とも言える.

match with文

match 式 with [|] パターン [when bool型の式] -> 式 {| パターン...}

ルール

  • 記述されている順に上からマッチしていき,マッチした時点で止まる

  • 変数名でマッチさせると,値はその名前の変数に束縛される

  • _ (アンダースコア)はワイルドカードとして,後で使わない部分とマッチさせられ る

文字列のリストをカンマ区切りで出力する例:

# let rec print_list list =
  match list with
    [] -> ""
  | [hd] -> hd
  | hd :: tl -> hd ^ "," ^ print_list tl;;
val print_list : string list -> string = <fun>
# print_list ["hello"; "ocaml"];;
- : string = "hello,ocaml"

let束縛

3つ以上の要素のタプルから何か取り出したかったら,以下のようにパターンマッチでき る:

# let t = (1, "Alice", 82);;
val t : int * string * int = (1, "Alice", 82)
# let (_, name, _) = t;;
val name : string = "Alice"

タプルの () は省略できる.

function構文

let 関数名 [引数...] = function パターンマッチ これは,match ... with の最後の引数を,自動的にパターンマッチの対象 にしてくれる構文.上の例を書き直すと少し見やすくなる:

# let rec print_list = function
    [] -> ""
  | [hd] -> hd
  | hd :: tl -> hd ^ "," ^ print_list tl;;
val print_list : string list -> string = <fun>

例外

例外は raise 例外名 で発生させられ, try 式 with [|] パターン [when bool式] -> 式 {| パターン...} で捕捉できる.

ゼロ除算の例外処理:

# let div x y =
    try
      Some (x / y)
    with
      Division_by_zero -> None;;
val div : int -> int -> int option = <fun>

例外は以下のような用途で使える

  1. 実際にエラーの場合

  2. 未定義の値を返したい場合: option型を使った方が安全だが,簡潔に書ける傾向にあ るらしい

  3. ループの脱出: ループ脱出の構文がないため例外を使ってジャンプできる

例外を使ったジャンプ:

# let data = open_in "file.txt" in
  try
    while true do
      let line = input_line data in
      Printf.printf "%s\n" line
    done
  with
    End_of_file -> ();;
hello ocaml
- : unit = ()

新しい例外は exception コンストラクタ [of 型名] で定義できる.

関数型言語らしい特徴

OCamlに限らず関数型言語ではループよりも再帰を利用して書くのが基本になる.

再帰関数

再帰的な関数を定義する際には,関数名の前に rec キーワードをつける. 階乗計算の例:

# let rec fact x =
    if x <= 1 then 1
              else x * fact (x - 1);;
val fact : int -> int = <fun>
# fact 5;;
- : int = 120

末尾再帰

ループと比べて,再帰は呼び出しが深くなるにつれてスタックを消費してしまうという問 題がある.しかし,もし再帰関数の処理が呼び出し元に戻ってこなくても終わるのであれ ば,スタックを保持しておく必要はないので解決できる. 言い換えれば,状態を引数として渡していけば呼び出し元の関数をスタックに残しておく 必要はなくなる.

これを 末尾再帰 (tail recursion) と呼び,末尾再帰の関数はループと全く同じよ うにコンパイルされる.

末尾再帰 ではない 例:

# let rec length ls =
match ls with
  [] -> 0
| hd::tl -> 1 + length tl;;
val length : 'a list -> int = <fun>

末尾再帰の例:

# let rec length' l n =
    match l with
      [] -> n
    | hd::tl -> length' tl (n+1);;
val length' : 'a list -> int -> int = <fun>

相互再帰

and キーワードを使って互いに呼び出し合う再帰関数も定義できる. これを相互再帰関数と呼ぶ.

奇数と偶数を判定する例:

# let rec odd x =
    if x = 0 then false
             else even (x - 1)
  and even x =
    if x = 0 then true
             else odd (x - 1);;
val odd : int -> bool = <fun>
val even : int -> bool = <fun>

高階関数

カリー化の例で,関数が関数を返したのと同じように,関数を引数としてとることもでき る.このような関数を高階関数(higher order function)という.

リストの要素全てに関数を適応した結果を返す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 fact [1; 3; 6; 10];;
- : int list = [1; 6; 720; 3628800]

リストの畳み込みを抽象化しているfold_leftの例:

# let rec fold_left f a l =
    match l with
      [] -> a
    | b::tl -> fold_left f (f a b) tl;;
val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = <fun>
# fold_left (+) 0 [1; 3; 6; 10];;
- : int = 20

クロージャ

クロージャ(closure)とは環境を伴った関数. 利点として以下がある:

  1. 関数間での情報共有のためにグローバル変数がいらなくなる

  2. 高階関数へ関数を渡す際に,引数を合わせるためにの無名関数を簡単に作れる

2の説明例として再利用を目的とした関数 retry を使う.これは整数 n と関数 f をもらって f() を成功するまで最大n回トライする関数:

# let retry n f =
  if n = 0 then
    raise (Invalid_argument "retry : n = 0")
  else
    let wrap f =
      try
        `Val (f ())
      with
        e -> `Err e
  in
  let rec loop n =
    match wrap f with
      `Val v -> v
    | `Err e ->
        if n = 1 then
          raise e
        else
          loop (n - 1)
  in
  loop n;;
val retry : int -> (unit -> 'a) -> 'a = <fun>

バッククオート始まっている `Val`Err多相ヴァリアント

retry関数が受け取る f の型は unit -> 'a になっているが,無名関数に包んで 渡すことで任意の型の関数を渡すことができるようになる:

# let read_file fname =
  retry 3 (fun () -> open_in fname);;
val read_file : string -> in_channel = <fun>

# let connect host fname = stdin;;
val connect : 'a -> 'b -> in_channel = <fun>
# let read_file_overnetwork host fname =
  try
    let fd =
      retry 3 (fun () -> connect host fname)
    in
    close_in fd
  with
    _ -> ();;
val read_file_overnetwork : 'a -> 'b -> unit = <fun>

型の詳細

OCamlは静的な型チェックを行う.また 型安全性 (type safety)と呼ばれる性質を 持ち,型の不一致が原因の実行時エラーは発生しないことが保障されている.

OCamlは型推論機能を持っているため,手で書く必要はない.

型宣言の応用

再帰型

定義する型にそれ自身を含められる

型変数

任意の型として宣言できる表現.(' (クォート)を付けた変数) 例えば空リストの型は 'a list となっていて,リストの要素はどんな型でも 構わないことを意味している.

例: 木構造の型:

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

# Node (Node (Leaf 1,
              Node (Leaf 2, Leaf 3)),
        Node (Node (Leaf 4, Leaf 5),
                    Leaf 6));;
- : int tree =
Node (Node (Leaf 1, Node (Leaf 2, Leaf 3)),
 Node (Node (Leaf 4, Leaf 5), Leaf 6))

Tree:

              <Node>
             ╱      ╲
       <Node>         <Node>
        ╱╲               ╱╲
  Leaf 1  ╲             ╱  Leaf 6
        <Node>        <Node>
         ╱╲              ╱╲
        ╱  ╲            ╱  ╲
  Leaf 2  Leaf 3    Leaf 4  Leaf 5

多相性の利用

let多相

let構文を利用して,束縛される関数にパラメータ多相を実現する仕組み:

# let fst (x, y) = x;;
val fst : 'a * 'b -> 'a = <fun>

# let double f x = f (f x);; (* 関数を2回適応 *)
val double : ('a -> 'a) -> 'a -> 'a = <fun>

多相性の制限

型安全を守るために,let多相には「部分適応で終わる場合には多相にならない場合があ る」という制約がある:

# let id x = x;;
val id : 'a -> 'a = <fun>

# let list_id = List.map id;; (* 単相型変数になる *)
val list_id : '_weak1 list -> '_weak1 list = <fun>
# let list_id' x = List.map id x;; (* 多相型になる *)
val list_id' : 'a list -> 'a list = <fun>

# list_id [1; 2; 3];;
- : int list = [1; 2; 3]
# list_id;;
- : int list -> int list = <fun> (* 型がintに固定されている *)
# list_id ['a'; 'b'; 'c'];; (* もうcharでは使えない *)
Error: This expression has type char but an expression was expected of type int

# list_id' [1; 2; 3];;
- : int list = [1; 2; 3]
# list_id' ['a'; 'b'; 'c'];;
- : char list = ['a'; 'b'; 'c']

list_id の型が '_weak1 となっているのは 単相型変数 といい, これは一度だけ型を代入できる型変数である.この例では最初にintのリストで呼び出し たことで固定され,charではエラーとなっている.

一方で,部分適応を使っていない list_id' は多相型となっているため,intでも charでも使うことができている.

多相ヴァリアント

多相的に使えて拡張性の高い ヴァリアント .バッククォート(`)を付けて定義す る:

type 型名 = [`コンストラクタ名 [of 型表現]] ...

多相ヴァリアントが有用な例として,ヴァリアントを後から拡張する場合を考える:

# type card = Num of int | Jack | Queen | King;;
type card = Num of int | Jack | Queen | King
# King;;
- : card = King
# type card' = Base of card | Joker;; (* card型にJokerを加える *)
type card' = Base of card | Joker
# Joker;;
- : card' = Joker
# Base King;;
- : card' = Base King

これに対してパターンマッチをする関数はネストしたコンストラクタを書かなければいけ なくなる:

# let to_number = function
  Joker -> max_int
| Base Jack -> 11
| Base Queen -> 12
| Base King -> 13
| Base (Num n) -> n
;;
val to_number : card' -> int = <fun>

# to_number Joker;;
- : int = 4611686018427387903
# to_number (Base Queen);;
- : int = 12
# to_number (Base (Num 8));;
- : int = 8

ここで,多相ヴァリアントを用いれば以下のように書くことができ, コンストラクタの増加を防ぐことができる:

# type card = [`Num of int | `Jack | `Queen | `King];;
type card = [ `Jack | `King | `Num of int | `Queen ]
# `King;;
- : [> `King ] = `King
# type card' = [card | `Joker];; (* card型にJokerを加える *)
type card' = [ `Jack | `Joker | `King | `Num of int | `Queen ]
# `Joker;;
- : [> `Joker ] = `Joker

先ほどと違って,`Jack などのコンストラクタは cardcard' の両方の 型に属している.パターンマッチもより簡潔に書くことができる:

let to_number = function
  `Joker -> max_int
| `Jack -> 11
| `Queen -> 12
| `King -> 13
| `Num n -> n
;;
val to_number : [< `Jack | `Joker | `King | `Num of int | `Queen ] -> int =
  <fun>

# to_number `Joker;;
- : int = 4611686018427387903
# to_number `Queen;;
- : int = 12
# to_number (`Num 8);;
- : int = 8

ただ,このような拡張をするにはベースとなる型を最初から多相ヴァリアントとして定義 しておかなければいけない.

モジュールシステム

モジュール

モジュールとは,複数の型宣言や式をセットにして分離できる機能:

module モジュール名 = struct モジュールの内容 end

例:
# module Counter = struct
    type t = int
    let make = 0
    let inc t = t + 1
    let dec t = t - 1
    let show = string_of_int
  end;;
module Counter :
  sig
    type t = int
    val make : int
    val inc : int -> int
    val dec : int -> int
    val show : int -> string
  end

モジュールの各機能は,モジュール名にドットを付けて呼び出すことができる:

# let x = Counter.make;;
val x : int = 0
# Counter.inc x;;
- : int = 1
# Counter.show x;;
- : string = "0"

モジュールを open することで,モジュール名を省略できるようになる:

# open Counter;;
# make;;
- : int = 0
# inc (inc (inc x));;
- : int = 3

モジュール間で名前の衝突があると,後にオープンしたモジュールの式が有効になるため 注意が必要.

モジュールは階層構造にすることもできる.

オブジェクト指向

標準ライブラリ

Troubles

missing graphics.cma

The Structure of OCaml Programs の例を動 かそうとしたが, "Error: Cannot find file graphics.cma" が出る場合.

ls `ocamlc -where`/graphics* でモジュールの存在を確認する.ない場合は, brew reinstall ocaml --with-x11 する必要がある.

graphics モジュールはオプショナルなので, brew install ocaml した場合は入ら ないみたい.しかもこれは外部モジュールではないので opam install graphics で はインストールできないからやや迷った. (ここ を見て解決した)