モジュールシステム
1. モジュール
ML には大規模なプログラムを適当な構成単位で分割して管理するためのモジュールシステムが用意されています. モジュールシステムの構成要素は次の 3 つです.
-
ストラクチャ(structure): いわゆる一般的なモジュール.
-
シグネチャ(signature): ストラクチャの仕様を記述したもの.
-
ファンクタ(functor): ストラクチャを生成する関数.
Standard ML の標準モジュールには,リスト関連の関数等を集めた List
ストラクチャ,文字列操作関数等を集めた String
ストラクチャなどがあります.
これらのストラクチャに含まれる関数等を利用するには,List.fileter
や String.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
ストラクチャでは,tkey
は int
,tval
はstring
に固定されていました.
今度はこれらをパラメータ化して,tkey
と tval
に任意の型をとれるようにしましょう.
ファンクタ (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
が連想リストの機能を提供していることがわかりますね.