LoginSignup
2
0

More than 5 years have passed since last update.

Standard MLのモジュールとシグネチャの機能

Last updated at Posted at 2017-12-15

これはML Advent Calendar 2017の15日目の記事です。
13日目の私の記事の続きになります。

本稿ではSMLのモジュール・シグネチャについて、より細かな機能を紹介いたします。

ファンクタ

先人の例であるOCmalでは、モジュールは第一級であり、関数や値と同等にモジュールを取り扱うことができます。
一方で、SMLのモジュールは第一級ではなく、モジュールがSMLコア(関数や式)とは切り離されて定義されています。
よって、モジュールを受け取ったり返したりするような関数をfun構文で作ることはできません。
しかしその代わりに、モジュールを受け取ったり返したりするために、SMLではモジュール専用のファンクタという機能が用意されています。

ファンクタを使うには、受け取るのと返すモジュールを記述します。1
例えば、実行時に単相のスタックを作るファンクタMakeMonoStackは以下のように書けます。
ELEMはファンクタ引数のモジュールを指定するシグネチャです。
MONOSTACKはファンクタ返り値のモジュールを指定するシグネチャです。

signature ELEM = sig type elem end
signature MONOSTACK =
sig
  type t
  type stack
  val create : unit -> stack
  val push : t * stack -> stack
  val pop : stack -> t * stack
end
functor MakeMonoStack (E : ELEM) : MONOSTACK =
struct
  type t = E.elem
  type stack = t list
  fun create () = nil
  fun push (x, s) = x :: s
  fun pop nil = raise Empty
    | pop (h::t) = (h, t)
end

ファンクタを利用してモジュールを受け取るには、以下のように関数適用のような構文を使用します。
関数適用と違って、括弧は省略できません。
ファンクタMakeMonoStackにモジュールElemを与えて、新たなモジュールMyStackを生成しています。

structure Elem = struct type elem = int end
structure MyStack = MakeMonoStack (Elem)
val s = MyStack.create ()
val s = MyStack.push (0, s)
val (x, s) = MyStack.pop s

不透明なシグネチャ制約

前章MonoStackや前記事Stackの問題点

前章で作ったファンタクタMakeMonoStackをREPLで入力してみると、下記のようにプリントされます。
同様に、前記事で作ったモジュールStackを少しだけ改変したMonoStackも入力してみます。

# functor MakeMonoStack (E : ELEM) : MONOSTACK =
> struct
>   type t = E.elem
>   type stack = t list
>   fun create () = nil
>   fun push (x, s) = x :: s
>   fun pop nil = raise Empty
>     | pop (h::t) = (h, t)
> end;
functor MakeMonoStack
  (sig
    type elem
  end)
  :
    sig
      type stack = 'elem list
      type t = 'elem
      val pop : 'elem list -> 'elem * 'elem list
      val create : unit -> 'elem list
      val push : 'elem * 'elem list -> 'elem list
    end
# structure MonoStack : MONOSTACK =
> struct
>   type t = int
>   type stack = t list
>   fun create () = nil
>   fun push (x, s) = x :: s
>   fun pop nil = raise Empty
>     | pop (h::t) = (h, t)
> end;
structure MonoStack =
  struct
    type stack = t list
    type t = int
    val pop = fn : t list -> t * t list
    val create = fn : unit -> t list
    val push = fn : t * t list -> t list
  end

プリント内容を見てみると、スタックがリスト型と表示されています。
つまり、モジュールの使用者に実装内容が見えてしまっており、悪意ある攻撃をする余地が残されています。
これはファンクタとモジュールのどちらでも同じ問題を抱えています。
前の記事のモジュールStackも、スタックに対してプリンタがint listと表示していました。

例えばこのとき、以下のようなことができてしまいます。
自分で用意したリストを使ってMonoStack.pushができてしまっています。
つまり、使用者が好きなようにスタックを改変できてしまいます。
これでは困る場合があります。

# val s = MonoStack.push (2, [0]);  (* 勝手に用意した[0] *)
val s = [2, 0] : int list
# (* ↑なんと、型チェックを通過してしまう!! *)

モジュール内の型を隠蔽する

そこでSMLでは、モジュールの型を隠蔽する機能が用意されています。
原文ではこの機能を「opaque constraint」と呼ばれ、日本語では「不透明なシグネチャ制約」と呼ばれています。
不透明な制約を使うには、先程の例でストラクチャにシグネチャを宣言した箇所の::>に変えます。

structure MonoStack :> MONOSTACK = (* ここが:>になっている! *)
struct
  type t = int
  type stack = t list
  fun create () = nil
  fun push (x, s) = x :: s
  fun pop nil = raise Empty
    | pop (h::t) = (h, t)
end

このスタックを使って先ほどと同じようなアタックを試みると、下記のような実行結果になります。

# structure MonoStack :> MONOSTACK =
> struct
>   type t = int
>   type stack = t list
>   fun create () = nil
>   fun push (x, s) = x :: s
>   fun pop nil = raise Empty
>     | pop (h::t) = (h, t)
> end;
structure MonoStack =
  struct
    type stack  <hidden>
    type t  <hidden>
    val pop = fn : stack -> t * stack
    val create = fn : unit -> stack
    val push = fn : t * stack -> stack
  end
# val s = MonoStack.create ();
val s = _ : MonoStack.stack
# val s = MonoStack.push (2, [0]);
(interactive):176.8-176.30 Error:
  (type inference 016) operator and operand don't agree
  operator domain: MonoStack.t * MonoStack.stack
          operand: 'CTYJ::{int, int8, int16, int64,...}
                   * 'CTYZ::{int, int8, int16, int64,...} list

先ほどの例とは違って、スタックの型が<hidden>と表示され、実体がプリントされなくなりました。
また、リストを使ってMonoStack.pushしようとすると、型エラーが起こっています。
つまり、MonoStack.stack型の実装は隠されれ、モジュールの外ではMonoStack.stack型とMonoStack.list型とは違う型として見えるようになりました。
これにより、使用者はMonoStack.createMonoStack.pushでしかMonoStack.stackを作ることができなくなりました。

一部の型だけ透明制約にする

これで型隠蔽でき、安全なスタックが作れました、とは行きません。
先ほどの例をよく見ると、もう一つ問題点があります。
モジュールMonoStackを使って、空のスタックを作って、MonoStack.pushしてみましょう。

# val s = MonoStack.create ();
val s = _ : MonoStack.stack
# val s = MonoStack.push (1, s);
(interactive):178.8-178.28 Error:
  (type inference 016) operator and operand don't agree
  operator domain: MonoStack.t * MonoStack.stack
          operand: 'CUZI::{int, int8, int16, int64,...} * MonoStack.stack

なんとMonoStack.pushが型エラーで弾かれてしまいます。
これは:>によって、モジュール内部のすべての型が不透明制約になってしまっているからです。
本当は、MonoStack.stackは不透明制約に、MonoStack.tは透明制約にしたいです。
このように、シグネチャに不透明制約と透明制約の両方の型を混在させたいことがよくあります。

これを解決するために、SMLではwhere構文を使って以下のように記述します。

(* シグネチャの後ろにwhere type t = int、を追加した! *)
structure MonoStack :> MONOSTACK where type t = int =
struct
  type t = int
  type stack = t list
  fun create () = nil
  fun push (x, s) = x :: s
  fun pop nil = raise Empty
    | pop (h::t) = (h, t)
end

これをREPLで動作させると、以下のようになります。
想定通り、MonoStack.stackの型だけが<hidden>になり、MonoStack.t型はint型であることが公開されています。
先ほど失敗したMonoStack.pushも想定通りの動作をしてくれます。

# structure MonoStack :> MONOSTACK where type t = int =
> struct
>   type t = int
>   type stack = t list
>   fun create () = nil
>   fun push (x, s) = x :: s
>   fun pop nil = raise Empty
>     | pop (h::t) = (h, t)
> end;
structure MonoStack =
  struct
    type stack  <hidden>
    type t = int
    val pop = fn : stack -> t * stack
    val create = fn : unit -> stack
    val push = fn : t * stack -> stack
  end
# val s = MonoStack.create ();
val s = _ : MonoStack.stack
# val s = MonoStack.push (1, s);
val s = _ : MonoStack.stack

モジュール・シグネチャの展開

大規模なプログラムを作成していると、他のモジュールやシグネチャの内容を再利用したいときがあります。
例えばよく使うのは、既にあるモジュールに新たな関数を追加して、新しいモジュールをつくる場合です。
先ほどのモジュールMonoStackに、スタックの空判定をする関数isEmptyを追加してみましょう。
これは、以下のように書けます。

signature MONOSTACK' =
sig
  include MONOSTACK  (* includeですでにあるシグネチャを使う *)
  val isEmpty : stack -> bool
end
structure MonoStack' =
struct
  open MonoStack  (* openですでにあるストラクチャを使う *)
  fun isEmpty stack =
    let val _ = pop stack in false end
    handle Empty => true
end

新たな関数isEmptyを追加したシグネチャMONOSTACK'とモジュールMonoStack'を定義しました。
ただし、元のMonoStackが不透明なシグネチャ制約のため、ここではスタックのデータ構造にアクセスできません。
そのため、例外処理をハックすることで、空判定を行っています。

includeはシグネチャ内でのみ使える構文で、シグネチャの中身が評価されて展開されます。2
一方で、openは通常の宣言としても使えます。
これは、モジュール内の関数や型を、モジュール名のプレフィクス無しに呼び出せるようにするための構文です。
モジュール内で使えば、既存モジュールの中身が展開され再利用ができます。

REPLで評価してみると、下記のようになります。
シグネチャやモジュールが拡張できている様子を確認できます。

# signature MONOSTACK' =
> sig
>   include MONOSTACK
>   val isEmpty : stack -> bool
> end;
signature MONOSTACK' =
  sig
    type stack
    type t
    val create : unit -> stack
    val push : t * stack -> stack
    val pop : stack -> t * stack
    val isEmpty : stack -> bool
  end
# structure MonoStack' =
> struct
>   open MonoStack
>   fun isEmpty stack =
>     let val _ = pop stack
>     in false end
>     handle Empty => true
> end;
structure MonoStack' =
  struct
    type stack  <hidden>
    type t = int
    val create = fn : unit -> stack
    val push = fn : t * stack -> stack
    val pop = fn : stack -> t * stack
    val isEmpty = fn : stack -> bool
  end
# val b = MonoStack'.isEmpty (MonoStack'.create ());
val b = true : bool

型の共有3

さらに大規模なプログラムを作成していると、より複雑な型の制約をつけたくなってきます。
既存のモジュールから新しいモジュールを作るとき、ただし一部の型が一緒の制限をつけたいことがあります。4

そんな場合はSMLのsharingを使います。
例えば下記のようなシグネチャが記述できます。
このシグネチャABでは、モジュールAにあるA.ta型とモジュールBにあるB.tb型が同値である制約をかけています。

signature AB =
sig
  structure A : sig type ta end  (* モジュールAはta型を持つ *)
  structure B : sig type tb end  (* モジュールBはtb型を持つ *)
  sharing type A.ta = B.tb  (* A.taとB.tbが同じ型という制約がつく *)
end

まとめ

本稿ではSMLのモジュール・シグネチャで使える構文と機能を紹介しました。

  • モジュールは第一級ではなく、動的モジュールを作る専用機能としてファンクタが用意されている。
  • 透明なシグネチャ制約」と「不透明なシグネチャ制約」を使って、シグネチャ内の型の公開・隠蔽を行う。
  • モジュールの展開にはopenを、シグネチャの展開にはincludeを使う。
  • シグネチャ内で同値な型を指定するためにはsharingを使う。

個人的な感想として、OCamlやHaskellに比べると、SMLの利用者は少ない感触を受けています。
今後、こういった記事から興味を持って、SMLerが一人でも多く増えることを祈っています。5

参考文献

前記事と同じです。

  1. Robin Milner, Mads Tofte, Robert Harper and David Macqueen. (1997). The Definition of Standard ML, Revised Edition. The MIT Press.
  2. 大堀 淳. (2001). プログラミング言語Standard ML入門. 共立出版.

ソースコード


  1. 実際は多くの複雑な派生構文の組み合わせによって構文が拡張されています。それにより、ファンクタはモジュールを受け取る形だけでなく、val xtype tなどを受け取る形でも書けます。後者のように書くと、内部ではstruct ... endが追加されて、モジュールを受け取るファンクタとして評価されています。 

  2. ここで言う展開はマクロではなく、評価による展開が行われます。 

  3. SMLのsharing機能の日本語記事はなかったので、おそらく本稿が世界初だと思います(言い過ぎ)。 

  4. コンパイラやパーサジェネレータなどの規模にならないと、そんなことは無いかもですが。例えば、マップやセットのキーを共有したりパーサジェネレータにおいてLexerとParserのtoken型を共有したりCharとChar関係モジュールでchar型を同値として取り扱ったりなどの例があります。 

  5. 具体的にはSMLのお仕事がもっと増えて、自分がSMLで簡単に食っていけるくらいに増えてください、増えろ。 

2
0
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
2
0