LoginSignup
7
4

More than 3 years have passed since last update.

OCaml 入門その4 モジュール・ファンクタ

Last updated at Posted at 2017-12-18

その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 のシグネチャ *)
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を学ぶ際にオススメです。

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4