OCaml

OCaml 入門その4

その3

モジュール

プログラムの部品だが、OCaml では ストラクチャと呼ぶ。
OCamlのライブラリは全てモジュール(ストラクチャ)として提供される。

ファイル名がモジュール名

example.ml というファイル名 => Example というモジュール。

標準モジュール

OCaml組み込みのモジュール郡

(* リスト *)
# List.length [1; 2; 3];;
- : int = 3
# let q = Queue.create ();;
val q : '_a Queue.t = <abstr>

(* キュー *)
# Queue.add "first" q;;
- : unit = ()
# Queue.take q;;
- : string = "first"
# Queue.take q;;
Exception: Queue.Empty.

(* 配列 *)
# Array.make 3 'a';;
- : char array = [|'a'; 'a'; 'a'|]

(* 標準出力 *)
# Printf.printf "%d, %x, %s\n" 10 255 "hoge";;
10, ff, hoge
- : unit = ()

open宣言

open モジュール名 と宣言することでモジュール名を省略可能。

python でいう from hoge import * に近い。

現在のソースにopenしたモジュールの名前空間が展開されるため、混乱しない時のみopenするようにする。
コンテナ系のモジュールは関数名が重なることが多いため、openしないのが普通。

(* open宣言 *)
# open List;;
# length [1; 2; 3];;
- : int = 3

(* 関数名を上書きする *)
# let length () = "overload!";;
val length : unit -> string = <fun>
# length ();;
- : string = "overload!"

(* モジュール名を指定すれば呼び出せる *)
# List.length [1; 2; 3];;
- : int = 3

ちなみに OCaml では起動時に Pervasives というモジュールが open されている。
absopen_in などの関数はこのモジュールに属する。

モジュール定義

module モジュール名 = struct いろいろ定義... end
モジュール名は大文字で始める。

(* モジュール定義 *)
# module Hello = struct
    let message = "Hello"
    let hello () = print_endline message
  end;;
module Hello : sig val message : string val hello : unit -> unit end
(* 呼び出し *)
# Hello.hello ();;
Hello
- : unit = ()

シグネチャ

  • シグネチャ
    • sig ... end で囲まれた部分
    • モジュール全体の型(のようなもの)
    • モジュールのI/Fを表す (アクセス可能なモジュールの要素を定義できる)

mli ファイル

Hogeモジュールのシグネチャは hoge.mli ファイルで定義可能。
ファイル内にシグネチャを記述する。

シグネチャの定義

定義 => module type シグネチャ名 = sig ... end
適用 => module モジュール名 : シグネチャ名 = モジュール名 または struct ... end

(* message 要素にアクセスできる *)
# Hello.message;;
- : string = "Hello"

(* messageが定義されていないシグネチャを定義する *)
# module type Hello_Sig =
    sig
      val hello: unit -> unit
    end;;
module type Hello_Sig = sig val hello : unit -> unit end

(* シグネチャを指定してモジュールを与える *)
# module Hello2 : Hello_Sig = Hello;;
module Hello2 : Hello_Sig

(* シグネチャにないため, message要素はアクセス不可 *)
# Hello2.hello ();;
Hello
- : unit = ()
# Hello2.message;;
Error: Unbound value Hello2.message

(* モジュールを直接定義するバージョン *)
# module Hello3 : Hello_Sig = struct
    let hello () = print_endline "Hello3!"
  end;;
module Hello3 : Hello_Sig
# Hello3.hello ();;
Hello3!
- : unit = ()
# Hello3.message;;
Error: Unbound value Hello3.message

抽象データ型

シグネチャ定義のとき、型定義(type t = ...)の = ... を省略すると
定義の詳細を隠蔽できる。

型情報と実装を隠蔽することで、その型に対する操作をモジュールを通じた操作に制限できる。
想定外の操作をできないようにする。

(* シグネチャ定義 *)
# module type AbstTypeSig = sig
    type t (* 抽象データ型 *)
    val get_t : int -> t
    val print : t -> unit
  end;;
module type AbstTypeSig =
  sig type t val get_t : int -> t val print : t -> unit end

(* モジュール定義 *)
# module AbstTypeInt : AbstTypeSig = struct
    type t = int
    let get_t i = i
    let print t = print_int t
  end;;
module AbstTypeInt : AbstTypeSig

(* 返り値が抽象データ型 <abstr> *)
# let t = AbstTypeInt.get_t 0;;
val t : AbstTypeInt.t = <abstr>
# AbstTypeInt.print t;;
0- : unit = ()

(* 
  抽象データ型は外部から扱えない
  AbstTypeInt.t は実質 int だが, 抽象データ型で隠蔽されているため
  print_int の引数に渡してもエラーになる
*)
# let () = print_int t;;
Error: This expression has type AbstTypeInt.t
       but an expression was expected of type int

ファンクター

パラメータを適用して動的にモジュールを生成する関数
一部だけ異なるモジュールを何度も定義する面倒をなくす。

ファンクターを使う

ファンクター適用 => ファンクター名 (モジュール式)

※ ファンクター適用自体もモジュール式

集合を扱う Set モジュールの例 

標準モジュールの SetQueue がファンクターを利用している。
扱いたい集合の要素に関して、以下のモジュールを定義する

  • 集合の要素のモジュールを定義
    • 集合の要素を表す型t
    • 要素型tの大小を比較する関数 compare: t -> t -> int

上記の「要素型のモジュール」をファンクターに「適用」して「その型を要素とす集合モジュール」を生成する。

(* ファンクター適用 *)
# module IntSet = Set.Make (struct
    type t = int
    let compare i j = i - j
  end);;
module IntSet :
  sig
    type elt = int (* 要素型として扱いたかった要素 elt = int *)
    type t         (* 集合を表す型は IntSet.t は抽象データ型 *)
    val empty : t
    val is_empty : t -> bool
    val mem : elt -> t -> bool
    val add : elt -> t -> t
    val singleton : elt -> t
    val remove : elt -> t -> t
    val union : t -> t -> t
    val inter : t -> t -> t
    val diff : t -> t -> t
    val compare : t -> t -> int
    val equal : t -> t -> bool
    val subset : t -> t -> bool
    val iter : (elt -> unit) -> t -> unit
    val fold : (elt -> 'a -> 'a) -> t -> 'a -> 'a
    val for_all : (elt -> bool) -> t -> bool
    val exists : (elt -> bool) -> t -> bool
    val filter : (elt -> bool) -> t -> t
    val partition : (elt -> bool) -> t -> t * t
    val cardinal : t -> int
    val elements : t -> elt list
    val min_elt : t -> elt
    val max_elt : t -> elt
    val choose : t -> elt
    val split : elt -> t -> t * bool * t
    val find : elt -> t -> elt
    val of_list : elt list -> t
  end

(* ファンクターから生成されたモジュールを利用する *)
# open IntSet;;
# let s1 = add 2 (add 1 empty)
  and s2 = add 1 (add 3 empty);;
val s1 : IntSet.t = <abstr>
val s2 : IntSet.t = <abstr>
# mem 1 s1;;
- : bool = true

ファンクターの定義

module ファンクタ名 (引数名 : シグネチャ式) = モジュール式

は以下の糖衣構文

module ファンクタ名 = functor(引数名 : シグネチャ式) -> モジュール式

  • Set.Make の簡易版のファンクタを定義する例
(* シグネチャの定義 *)
module type ELEMENT = sig
  type t
  val compare: t -> t -> int
end

(* ファンクタの定義 *)
module MakeSet (Element : ELEMENT) =
  struct
    type elt = Element.t
    type t = elt list

    let empty = []

    let mem x set = List.exists (fun y -> Element.compare x y = 0) set

    let rec add elt = function
    | [] -> [elt]
    | (x :: rest as s) ->
        match Element.compare elt x with
        | 0 -> s
        | r when r < 0 -> elt :: s
        | _ -> x :: (add elt rest)

    let rec elements s = s
  end;;

依存型

  • 上記のファンクタの返り値のシグネチャは以下
    • functor (Element : ELEMENT) -> の記述移行の sig ... end の中を見ると
    • type elt = Element.t と記述されている
    • 仮引数の値が返り値の型に含まれている
      • つまり, 与える引数の値(!=型) に応じて返り値の型が変化する
      • これを 依存型 という
(* トップレベルからの返り値 *)
module type ELEMENT = sig type t val compare : t -> t -> int end
module MakeSet :
  functor (Element : ELEMENT) ->
    sig
      type elt = Element.t
      type t = elt list
      val empty : 'a list
      val mem : Element.t -> Element.t list -> bool
      val add : Element.t -> Element.t list -> Element.t list
      val elements : 'a -> 'a
    end

ファンクタの情報隠蔽

通常のモジュールと同様に、ファンクターの返り値のシグネチャを制限し内部実装を隠蔽可能。

  • module ファンクタ名 (引数名 : 入力シグネチャ式) : 返したいシグネチャ式 = モジュール式
    • 返したいシグネチャ式を指定することで情報隠蔽可能
module MakeSet (Element : ELEMENT) :
  (* 返したいシグネチャ式 *)
  sig
    type elt = Element.t
    type t (* 抽象データ型 *)
    val mem : elt -> t -> bool
    val add : elt -> t -> t
    val elements : t -> elt list
  end
  =
  (* モジュール式 *)
  struct
    type elt = Element.t
    type t = elt list

    let empty = []

    let mem x set = List.exists (fun y -> Element.compare x y = 0) set

    let rec add elt = function
    | [] -> [elt]
    | (x :: rest as s) ->
        match Element.compare elt x with
        | 0 -> s
        | r when r < 0 -> elt :: s
        | _ -> x :: (add elt rest)

    let rec elements s = s
  end;;

(* ファンクター式の返り値 *)
module MakeSet :
  functor (Element : ELEMENT) ->
    sig
      type elt = Element.t
      type t
      val empty : t
      val mem : elt -> t -> bool
      val add : elt -> t -> t
      val elements : t -> elt list
    end

上記のファンクター適用式の返り値のシグネチャ式を比較すると

  • Before
# module StringSet = MakeSet(String);;
module StringSet :
  sig
    type elt = String.t
    type t = elt list
    val empty : 'a list
    val mem : String.t -> String.t list -> bool
    val add : String.t -> String.t list -> String.t list
    val elements : 'a -> 'a
  end
  • After

# module IntSet = MakeSet(struct
    type t = int
    let compare i j = i - j
  end);;
module IntSet :
  sig
    type elt = int
    type t (* 抽象データ型 *)
    val empty : t
    val mem : elt -> t -> bool
    val add : elt -> t -> t
    val elements : t -> elt list
  end

# Open IntSet;;
# let s1 = add 1 (add 2 empty)
  and s2 = add 3 (add 4 empty);;
val s1 : IntSet.t = <abstr> (* 抽象データ型 *)
val s2 : IntSet.t = <abstr>

(* 文字列のとき *)
# module StringSet = MakeSet(String);;
module StringSet :
  sig
    type elt = String.t
    type t = MakeSet(String).t (* 何か実装が隠されていない? *)
    val empty : t
    val mem : elt -> t -> bool
    val add : elt -> t -> t
    val elements : t -> elt list
  end

# open StringSet;;
# let s1 = add "a" (add "b" empty)
  and s2 = add "c" (add "d" empty);;
val s1 : StringSet.t = <abstr> (* 抽象データ型にはなっている *)
val s2 : StringSet.t = <abstr>

シグネチャの分解・定義 (with type)

シグネチャ式 with type 型名 = 型定義 and type ...

  • ファンクターの返したいシグネチャ定義を sig ... end で直接指定せず、名前で与えたい。
    • type elt = Element.t の部分
    • 仮引数名に依存しているためシグネチャを別名定義できない
    • エラーになる
module MakeSet (Element : ELEMENT) :
  (* 返したいシグネチャ式 *)
  sig
    type elt = Element.t
    type t (* 抽象データ型 *)
  end
  =
  struct ... end;;

with type構文を使う

(* with type で置換したい型定義 elt *)
# module type ElementWithType =
    sig
      type elt
      type t
      val empty : t
      val mem : elt -> t -> bool
      val add : elt -> t -> t
      val elements : t -> elt list
    end;;

(* with type で定義を与える *)
# module type E2 = ElementWithType with type elt = int;;
module type E2 =
  sig
    type elt = int
    type t
    val empty : t
    val mem : elt -> t -> bool
    val add : elt -> t -> t
    val elements : t -> elt list
  end

バッチコンパイラと分割コンパイル

ファイル単位で書き出すコンパイラ => バッチコンパイラ
ファイル単位でオブジェクトファイルを作成しリンク => 分割コンパイル

ocaml のバッチコンパイラ => ocamlc(バイトコード), ocamlopt(ネイティブコード)

まとめてコンパイル

$ ocamlc -o output.name src1.ml src2.ml ...

分割コンパイル

  • 最後のリンク時の cmo ファイルは名前解決ができるように並べる必要がある
  • 非標準ライブラリ
    • リンク時にファイル名を明示する必要あり
    • nums.cma など

-c はリンクしないでオブジェクトファイルを出力するオプション
-Icmi, cmo をインクルードするディレクトリを指定可能

$ ocamlc -c mod1.ml
$ ocamlc -c mod2.ml
$ ocamlc -o output.name nums.cma mod1.cmo mod2.cmo

mliファイル

mli ファイルは対応する .ml のシグネチャを記述したもの
コンパイル時は ocamlc を使う

  1. ocamlcmli ファイルをコンパイル
  2. ocamlc -cml ファイルをコンパイル
  3. ocamlccmo をリンク
$ ocamlc mod.mli  # -c オプションは不要
$ ocamlc -c mod.ml
$ ocamlc -c mod2.ml
$ ocamlc -o a.out mod.cmo mod2.cmo

参考文献

プログラミング in OCaml

非常に分かりやすく、OCamlを学ぶ際にオススメです。