SML#のexistential typeで遊ぶ

κeenです。SML# 3.7.0から実験的機能として _dynamiccase に存在型のサポートが入ったので試してみます。

本記事はSML# 4.0.0 with LLVM 12.0.0で動作確認を行っています。

Dynamicについて

_dynamiccase 以前にDynamicの説明から入りましょう。 DynamicはSML# 3.5.0から入った機能です。 型を消して代わりにデータに埋め込んでおき、使うときにその型を復元して使えるものです。

型を消す(型をデータに埋め込む)のは Dynamic.dynamic などのプリミティブを、型を復元するのは _dynamic などの制御構造を使います。

val x = Dynamic.dynamic {a = 1, b = "c"}
(* val x = _ : Dynamic.void Dynamic.dyn *)

val y = _dynamic x as {a:int, b:string}
(* val y = {a = 1, b = "c"} : {a : int, b : string} *)

Dynamicは色々面白い使い方ができます。一旦型情報をデータにしてしまえばリフレクションが可能なのでどんな型にも使えるpretty printも実装可能になります。

Dynamic.pp {a = 1, b = "c"}
(* => {a = 1, b = "c"} *)

他にも面白い話題はありますが、今回のトピックは _dynamiccase なのでそれに触れます。 _dynamiccase_dynamic のパターンマッチ版で、マッチした型を復元します。

val x = Dynamic.dynamic {a = 1, b = "c"}
_dynamiccase x of
    {a: int, b: int}    => "first" (* 変数aとbが使える *)
  | {a: int, b: string} => "second"
(* "second" *)

ここまで、Dynamicと _dynamiccase の紹介でした。

_dynamiccase のexistential typeサポート

SML# 3.7.0から入った実験的なexistential typeサポートでは _dynamiccase で存在型が使えます。 節の冒頭で {'a} の形で型変数を導入し、パターン内でその 'a が使えます。 パターンの腕では {'a} の中身は分からりません。 'a 型がついた値はパターンの腕からは脱出できないので関数を値に適用するなどして型を潰す必要があります。

fun apply x = _dynamiccase x of {'a} (f:'a -> int, x:'a) => f x;

apply (Dynamic.dynamic (foldl (op +) 0, [1, 1]));
apply (Dynamic.dynamic (trunc, 2.34));

上記の例では 'a にそれぞれ intreal が代入されますが、最終的には int を返しています。 ちょっと使い道が分かりづらいかもしれませんが、いくつか面白いことができます。

ダックタイピング

存在型を使うとダックタイピングができます。 ダックタイピングは「アヒルのように鳴きアヒルのように歩くならそれはアヒルだ」という例えに表されるようにデータの型ではなくメッセージを受け取れるかに着目します。 これからデータの型は異なるが同じ振る舞いをするオブジェクトを作っていきます。

まずは準備からはじめましょう。必須ではないですが、分かりやすいようDynamicをラップした型を定義しておきます。

datatype obj = Obj of Dynamic.dynamic

toString を呼べるオブジェクトを作りたいのでそのオブジェクトを表わす型を作っておきます。データとメソッドのレコードです。

type 'self objRec = {data: 'self, toString: 'self -> string}

型パラメータに 'self と名付けているように、data がいわゆるselfのつもりです。ユーザがこの型を用意したらオブジェクトが作れます。

雰囲気を出すために new 関数も用意しておきましょう。

fun 'self#reify new (obj: 'self objRec) = Obj (Dynamic.dynamic obj)

'self#reify という修飾がついているのはSML# 独自の拡張です。 型をデータとして書き込むために(=Dynamic.dynamicを呼ぶために)型情報を取り出せる必要があるのでついています。ここはあまり深くは踏み込まずに次に進みます。

この new で作ったデータ型に対して toString を呼べるようにしてみます。 dynamic_dynamiccase にかけてデータを復元します。このとき多相になっていた 'self を存在型で表現してあげるとうまくいきます。

fun toString (Obj obj) =
    _dynamiccase obj of
      {'self} {data: 'self, toString: 'self -> string} => toString data

selfとselfを第一引数に取るメソッドがあればそれっぽくなる、というのはある程度ダックタイピングに慣れた人なら理解頂けると思います。

これで役者が揃ったのでREPLで使ってみましょう。まずは new を使ってオブジェクトをいくつか作ってみます。

# val intObj  = new {data = 1,   toString = Int.toString};
val intObj = Obj _ : obj
# val realObj = new {data = 0.1, toString = Real.toString};
val realObj = Obj _ : obj

intObjrealObj はそれぞれ型が異なりますが、どちらも obj になっています。これらの objtoString を呼んでみましょう。

# toString intObj;
val it = "1" : string
# toString realObj;
val it = "0.1" : string

元の型は異なるのに1つの関数で双方を扱えました。intObjrealObj は同じ obj 型なのでリストに共存させることも可能です。

# map toString [intObj, realObj];
val it = ["1", "0.1"] : string list

こうしてSMLでダックタイピングができるようになりました。

拡張可能なオブジェクト

上記のオブジェクトは面白いんですが、フィールドが増えたときに _dynamiccase に失敗するという問題があります。

例えばメソッド toInt を増やした場合にオブジェクトの実体はこうなります。

type 'self objRec = {data: 'self, toString: 'self -> string, toInt: 'self -> int}

しかし toString がそのままだと toInt フィールドがないのでマッチせずに呼出に失敗してしまいます。 必要なフィールドは揃っているはずなのにもどかしいですよね。そこで、必要なオブジェクトだけを取り出すようにしてみます。

_dynamiccase には余計なフィールドを無視する機能はなさそうでした。例えばレコードのパターンマッチに ... を使うとICEになります。

fun toString (Obj obj) =
    _dynamiccase obj of
      {'self} {data: 'self, toString: 'self -> string, ...} => toString data

(* Bug.Bug: PolyTyElimination: compileTy: BOUNDVARty at src/compiler/compilePhases/polytyelimination/main/PolyTyElimination.sml:17.14(302) *)

そこでDynamic側をいじって必要なデータだけを持つDynamicを作ることにします。 このあたりはSML#のリフレクションAPIに踏み込むので深くは触れませんが、以下のようなキャスト関数を定義すると上手くいきそうでした。

fun cast dyn labels = let
    val terms = map (fn label => (label, Dynamic.dynamicToTerm (Dynamic.## label dyn))) labels
in
    Dynamic.termToDynamic (Dynamic.Record terms)
end

このキャストを使って objtoString フィールドだけを抜き出すようにした toString を定義してみます。

fun toString (Obj obj) =
    _dynamiccase (cast obj ["data", "toString"]) of
      {'self} {data: 'self, toString: 'self -> string} => toString data

_dynamiccase にかける前に cast を呼ぶことで必要なフィールドのみ取り出しています。 同様に toInt も定義可能なので定義しておきます。

fun toInt (Obj obj) =
    _dynamiccase (cast obj ["data", "toInt"]) of
      {'self} {data: 'self, toInt: 'self -> int} => toInt data

これでREPLで試しましょう。 intObjrealObj を再定義して toInt も持つようにします。

# type 'self objRec = {data: 'self, toString: 'self -> string, toInt: 'self -> int};
# fun 'self#reify new (obj: 'self objRec) = Obj (Dynamic.dynamic obj);
# val intObj  = new {data = 1,   toString = Int.toString,  toInt = fn x => x};
# val realObj = new {data = 0.1, toString = Real.toString, toInt = Real.round};

Dynamicを使って既存のオブジェクトを拡張することもできますが、ちょっとややこしいので素直に再度定義します。

これらの拡張したオブジェクトを toStringtoInt に適用します。

# toString intObj;
val it = "1" : string
# toInt realObj;
val it = 0 : int

呼べていますね。ということでSML#でダックタイピングでした。

このあと真面目にオブジェクト指向っぽくやるなら作り込み要素はいっぱいありますが、遊ぶだけならこれで十分でしょう。

第一級モジュールっぽいやつ

思ったよりダックタイピングで疲れたのでこっちは雑にいきます。

第一級モジュールはモジュールを値として扱えるやつです(雑)。 OCamlとかにはありますが、SMLにはありません。

モジュールを値として扱うときはほとんどレコードのようなものにエンコードできます。 val がそのままレコードにエンコードされるのですが、 type は存在型へエンコードすることになります。 そこで _dynamiccase の存在型を使うとそれっぽくなるんじゃないかという趣旨です。 第一級モジュールの存在型周りはかなりややこしかった気がしますが疲れたのでそれっぽければ良いことにします。

まずはエンコードする前のモジュールのシグネチャと構造を提示します。

signature Color = sig
    type t
    val toCode: t -> string
end

structure RGBColor: Color = struct
datatype t = RGB of word8 * word8 * word8
fun toCode (RGB (r, g, b)) =
    let fun to c = if c <= 0wxff
                   then "0" ^ (Word8.toString c)
                   else Word8.toString c
    in "#" ^ (to r) ^ (to g) ^ (to b) end
end

はい。これをレコード(など)にエンコードしていきましょう。 初っ端からインチキですが、モジュールのレコードと、t の値の両方をデータに持つことにします。

type 'a fstColor = {color: 'a, toCode: 'a -> string}

例によって雰囲気出すためにpackを関数を作っておきます。

fun 'a#reify packColor (m: 'a fstColor) = Dynamic.dynamic m

あとはなんか _dynamiccase でunpackするだけ!

fun toCode d = _dynamiccase d of {'a} {color: 'a, toCode: 'a -> string} => toCode color

M.t とかの型を書けないのでダックタイピングと一緒ですね。ダメだったよ…

まとめ

SML# 3.7.0で入った _dynamiccase の存在型のサポートを使ってダックタイピングをやってみました。 試してないですが、reificationで型情報を取り出せるのでクラスやプロトタイプの仕組みを知っている人なら自分でオブジェクト指向っぽい枠組みを作れるんじゃないかと思います。

Written by κeen