モジュールシステム

1. モジュール

ML には大規模なプログラムを適当な構成単位で分割して管理するためのモジュールシステムが用意されています. モジュールシステムの構成要素は次の 3 つです.

  • ストラクチャ(structure): いわゆる一般的なモジュール.

  • シグネチャ(signature): ストラクチャの仕様を記述したもの.

  • ファンクタ(functor): ストラクチャを生成する関数.

Standard ML の標準モジュールには,リスト関連の関数等を集めた List ストラクチャ,文字列操作関数等を集めた String ストラクチャなどがあります. これらのストラクチャに含まれる関数等を利用するには,List.fileterString.tokens のようにストラクチャ名による名前の修飾が必要です.

- List.filter (fn n => n mod 2 = 0) [1,2,3,4,5,6,7,8];
val it = [2,4,6,8] : int list

- String.tokens (fn c => c = #"/") "/usr/local/bin";
val it = ["usr","local","bin"] : string list

ただし,次のように open 宣言を使うと,それ以降そのストラクチャ名の修飾を省略することができます.

- open List;
...

- filter (fn n => n mod 2 = 0) [1,2,3,4,5,6,7,8];
val it = [2,4,6,8] : int list

2. ストラクチャ

次のプログラムは,連想リスト(alist)を実現するストラクチャ Alist を記述したものです. このストラクチャでは,1 つの例外,3 つのデータ型,4 つの関数を定義しています.

(* file: alist1.sml *)

(* ストラクチャ Alist の定義 *)
structure Alist =
struct
  exception AlistExn

  type tkey = int                  (* キーの型 *)
  type tval = string               (* 値の型 *)
  type alist = (tkey * tval) list  (* 連想リストの型 *)

  (* 新規リストを作成 *)
  fun new () = nil : alist

  (* 要素を追加 (キーが重複していれば例外発生) *)
  fun add (k,v) ls = if exists k ls then raise AlistExn else (k,v)::ls

  (* キー key が存在すれば true *)
  and exists key [] = false
    | exists key ((k,v)::ls) = k = key orelse exists key ls

  (* キー key に対応する値を返す *)
  fun find key [] = raise AlistExn
    | find key ((k,v)::ls) = if k = key then v else find key ls
end

対話環境に読み込んで,少し遊んでみましょう.

- use "alist1.sml";
...
- open Alist;
...

- val ls = new ();
val ls = [] : alist
- val ls = add (1, "one") ls;
val ls = [(1,"one")] : (tkey * string) list
- val ls = add (2, "two") ls;
val ls = [(2,"two"),(1,"one")] : (tkey * string) list

- find 1 ls;
val it = "one" : string
- find 2 ls;
val it = "two" : string
- find 3 ls;
uncaught exception AlistExn
  raised at: alist1.sml:22.27-22.35

キーワード structure から end まではストラクチャ式と呼ばれ,この中には関数宣言や型宣言など様々な宣言を記述できます(念のため付言すると,これ以外の形のストラクチャ式も存在します). ストラクチャに名前をつけるには,structure ストラクチャ名 = ストラクチャ式 という形の宣言を記述します.

3. シグネチャ

対話環境でストラクチャの宣言を行うと,次のように表示されることが確認できます.

- use "alist1.sml";
[opening alist1.sml]
structure Alist :
  sig
    exception AlistExn
    type tkey = int
    type tval = string
    type alist = (tkey * tval) list
    val new : unit -> alist
    val add : ''a * 'b -> (''a * 'b) list -> (''a * 'b) list
    val exists : ''a -> (''a * 'b) list -> bool
    val find : ''a -> (''a * 'b) list -> 'b
  end
val it = () : unit

sig から end までがストラクチャ Alistシグネチャ(signature)と呼ばれるものです. シグネチャには,ストラクチャで公開される名前に関する情報が記述されます.

独自のシグネチャを定義することで,ストラクチャで公開される名前を選択することができます.

次のプログラムは,独自のシグネチャ ALIST を定義した例です. ストラクチャ Alist には ALIST によるシグネチャ制約を加えています. これによって,シグネチャに記述されていない名前 exists がストラクチャの外部には非公開となり,隠蔽されます.

(* file: alist2.sml *)

(* シグネチャ ALIST の定義 *)
signature ALIST =
sig
  exception AlistExn
  eqtype tkey
  type tval
  type alist
  val new : unit -> alist
  val add : tkey * tval -> alist -> alist
  val find : tkey -> alist -> tval
end

(* ストラクチャ Alist の定義 *)
structure Alist : ALIST =  (* シグネチャ制約を付加 *)
struct
  (* alist1 と同じ *)
end

sig から end まではシグネチャ式と呼ばれ,この中に公開すべきストラクチャの仕様を記述します. シグネチャに名前をつけるには,signature シグネチャ名 = シグネチャ式 という形の宣言を記述します.

対話環境でこのプログラムを読み込むと,以下のように表示されます.

- use "alist2.sml";
[opening alist2.sml]
signature ALIST =
  sig
    exception AlistExn
    eqtype tkey
    type tval
    type alist
    val new : unit -> alist
    val add : tkey * tval -> alist -> alist
    val find : tkey -> alist -> tval
  end
structure Alist : ALIST
val it = () : unit

表示されるシグネチャに名前 exists が現れていないことが確認できます. 実際に Alist.exist 1 ls を評価してみても,エラーが発生することが確認できます.

- val ls = Alist.new ();
val ls = [] : Alist.alist

- val ls = Alist.add (1, "one") ls;
val ls = [(1,"one")] : Alist.alist

- Alist.exist 1 ls;
stdIn:25.1-25.6 Error: unbound variable or constructor: exist in path Alist.exist

4. シグネチャ制約の透明性

ストラクチャのシグネチャ制約に用いる ::> に置き換えると,シグネチャ制約の透明性を変更できます. : による制約は透明な制約 (transparent constraint),:> による制約は不透明な制約 (opaque constraint) と呼びます. 制約の透明性がどのようなものであるかは,例を見ればお分かりいただけるでしょう.

(* file: alist3.sml *)

signature ALIST =
sig
  (* alist2 と同じ *)
end

structure Alist :> ALIST =  (* 不透明なシグネチャ制約 *)
struct
  (* alist2 と同じ *)
end

このプログラムを対話環境に読み込んでみます.

- use "alist3.sml";
...

- val ls = Alist.new ();
val ls = - : Alist.alist

透明な制約では連想リストの中身が表示され,連想リストの実装が普通のリストであることがバレていますが,不透明な制約を用いることで alist 型の実体を隠蔽することに成功しています. ただし,この例の不透明なシグネチャ制約は他の型の実体も隠蔽するため,次のように tkey 型と int 型,tval 型と string 型を同じ型と見なしてくれなくなります.

- val ls = Alist.new ();
val ls = - : Alist.alist

- val ls = Alist.add (1, "one") ls;
stdIn:31.5-31.33 Error: operator and operand don't agree [literal]
  operator domain: Alist.tkey * Alist.tval
  operand:         int * string
  in expression:
    Alist.add (1,"one")

これを解決するには,次のように where type で始まる記述を追加して,選択的に型の実体を公開します. なお,この記述はシグネチャ式の直後に置くことができます.

(* file: alist4.sml *)

signature ALIST =
sig
  exception AlistExn
  eqtype tkey
  type tval
  type alist
  val new : unit -> alist
  val add : tkey * tval -> alist -> alist
  val find : tkey -> alist -> tval
end
  where type tkey = int      (* tkey は int と同じだよ *)
  where type tval = string   (* tval は string と同じだよ *)

structure Alist :> ALIST =   (* 不透明なシグネチャ制約 *)
struct
  (* alist2 と同じ *)
end

対話環境での実行結果は次の通りです. alist 型の実体が隠蔽されつつ,tkey, tval 型が適切に実行環境に認識されていることが分かります.

- use "alist4.sml";
...

- val ls = Alist.new ();
val ls = - : Alist.alist

- val ls = Alist.add (1, "one") ls;
val ls = - : Alist.alist

- val ls = Alist.find 1 ls;
val ls = "one" : Alist.tval

5. ファンクタ

これまで見てきた Alist ストラクチャでは,tkeyinttvalstringに固定されていました. 今度はこれらをパラメータ化して,tkeytval に任意の型をとれるようにしましょう.

ファンクタ (functor) はストラクチャにパラメータを持たせたものです. 以下の例では,2つのパラメータを持つファンクタ Alist を定義しています.

(* file: alist5.sml *)

(* シグネチャ ALIST の定義 *)
signature ALIST =
sig
  exception AlistExn
  eqtype tkey
  type tval
  type alist
  val new : unit -> alist
  val add : tkey * tval -> alist -> alist
  val find : tkey -> alist -> tval
end

(* ファンクタ Alist の定義 *)
functor Alist (eqtype tk type tv)
  :> ALIST                 (* 不透明なシグネチャ制約 *)
  where type tkey = tk     (* tkey は tk と同じだよ *)
  where type tval = tv  =  (* tval は tv と同じだよ *)
struct
  exception AlistExn

  type tkey = tk
  type tval = tv
  type alist = (tkey * tval) list

  fun new () = nil

  fun add (k,v) ls = if exists k ls then raise AlistExn else (k,v)::ls

  and exists key [] = false
    | exists key ((k,v)::ls) = k = key orelse exists key ls

  fun find key [] = raise AlistExn
    | find key ((k,v)::ls) = if k = key then v else find key ls
end

(* ファンクタからストラクチャを生成 *)
structure IntStrAlist = Alist (type tk = int type tv = string)

ファンクタの宣言は,structure の代わりに structure が使われることと,ファンクタ名の直後にパラメータリストが入ること以外は,基本的にストラクチャの宣言と同じです. ファンクタ名の直後の括弧 (…​) の中には sig…​end の中に書けるものと同じものを書くことができ,これがパラメータリストの役割を果たします.

ファンクタからストラクチャを生成するには,Alist (type tk = int type tv = string) のように,ファンクタ名に続いて括弧の中に宣言を記述します. この括弧の中には struct…​end の中に書けるものと同じものを書くことができます.

このプログラムを対話環境で動かすと,以下のようになります.

- use "alist5.sml";
...
- open IntStrAlist;
...

- val ls = new ();
val ls = - : alist

- val ls = add (1, "one") ls;
val ls = - : alist

- find 1 ls;
val it = "one" : tval

期待通り,IntStrAlist が連想リストの機能を提供していることがわかりますね.