SML#のoverLoad
多くの犠牲者参加者を生んだLL/ML Advent Calendarの12/3だよ!
詳しくはこちらのイベントページで→partake.in
今回のお題はSML#のオーバーロードについて!
SML#はver0.5からオーバーロード(多重定義)の機能が実装されました.
限られた型のみが許される多相性を関数に対して持たせることができます.
例えば+は以下の様に定義されており,intやrealなどある制限された型に対して多相的に適用できます.
SML# version 1.1.0 () for x86-linux # 1 + 2; val it = 3 : int # 1.5 + 4.3; val it = 5.8 : real # op +; val it = fn : ['a::{int, word, SMLSharp.Word8.word, SMLSharp.IntInf.int, real, SMLSharp.Real32.real}. 'a * 'a -> 'a]
ちなみにSML/NJでは以下のように動作します.
+は複数の型に対して適用できますが,多相型を持つような使用は許されていません.
+の型が必要になると,予め決められたデフォルト型(今回の例ではint)が選択されます.
Standard ML of New Jersey v110.72 [built: Tue Mar 9 00:18:58 2010] - 1 + 2; val it = 3 : int - 1.5 + 4.3; val it = 5.8 : real - op +; val it = fn : int * int -> int
さてさて,ここからが本題.
SML#のオーバーロード,実装当初のver0.50では組み込みの特殊な関数でした.
しかしver0.90からは自分でオーバーロードされた関数を作ることができます.
smiファイル内で,定義したい関数に受け取る型によって,多重定義する関数を書いてやればいいのです.
例えば以下のようなオーバーロードされたtoStringが作れます.
(* overload.sml *) fun intToLine n = Int.toString n ^ "\n"
(* overload.smi *) _require "basis.smi" val toString = case 'a in 'a -> string of int => intToLine | real => Real.toString
(* main.sml *) val () = print (toString 1) val () = print (toString 1.0 ^ "\n")
(* main.smi *) _require "basis.smi" _require "overload.smi"
$ smlsharp -c overload.sml $ smlsharp -c main.sml $ smlsharp main.smi $ ./a.out 1 1.0
overload.smiで定義したtoStringがintにもrealにも適用できて無事プリントされています.
やったね!
ちなみにsmlファイルを評価してから,smiファイルを評価する順序のようです.
よって,smlファイルで定義した関数をsmiファイル内で指定することは可能です.
smiファイルで定義したオーバーロードな関数は自身のsmlファイルでは呼び出せません.
smiファイル内では,複数の関数に割り振る(多重定義)というイメージで,既に定義されている関数名をcaseで割り振ります.
ここでfn(ラムダ式)とか関数定義は書けません.
さて,ここまでではeldeshさんの焼きましに過ぎません.*1
SML#のオーバーロードをもっと便利に使えないか考えてみました.
smiファイルの文法のオーバーロードの部分は大体以下のようになっています.
詳しくはinterface.grmファイルを読め!
(* 大文字が非終端記号,小文字が終端記号 *) OVERLOAD_EXP := case TYPE_VARIABLE in TYPE OVERLOAD_MATCHES OVERLOAD_MATCHES := TYPE => OVERLOAD_MATCH | TYPE => OVERLOAD_MATCH | OVERLOAD_MATCHES OVERLOAD_MATCH := TYPE => OVERLOAD_EXP | TYPE => ( OVERLOAD_EXP ) | TYPE => ID | TYPE => ( ID )
fnとかがまったくない,シンプルな文法ですね.
ただし,caseのネストが許されています.
そこでcaseをネストして,intとrealを複合して足せるaddを定義してみようとしてみます.
以下のような関数を書いてみましたが,うまくいきません.
(* overload2.sml *) fun addIntInt a b = Real.fromInt a + Real.fromInt b fun addIntReal a b = Real.fromInt a + b fun addRealInt a b = a + Real.fromInt b fun addRealReal a b = a + b
(* overload2.smi ダメな例(´・∀・`) *) _require "basis.smi" fun addIntInt (a, b) = Real.fromInt a + Real.fromInt b fun addIntReal (a, b) = Real.fromInt a + b fun addRealInt (a, b) = a + Real.fromInt b fun addRealReal (a, b) = a + b val add2 = case 'a in 'a * 'b -> real of int => (case 'b in 'a * 'b -> real of int => addIntInt | real => addIntReal) | real => (case 'b in 'a * 'b -> real of int => addRealInt | real => addRealReal)
$ smlsharp -c overload.sml overload.smi:16.14-18.33 Error: user type variable 'b is scoped at outer declaration overload.smi:19.15-21.35 Error: user type variable 'b is scoped at outer declaration
ちなみにSML#のオーバーロードは多相レコードの実装に使われているタイプカインドを利用して実装してあります.*2 *3
レコードの多層型は複数種類の型パラメータを持てるのだから,オーバーロードだけ単数種類の型パラメータに限定する理由は無いと思うのですけど,よくわかりません.*4
とも思ったけど,このadd2はよく考えると型が書けないことに気づきました.
今回の例なら以下のような型が付いてくれると嬉しいです.
val add2 : 'a::{int, real}, 'b::{int, real}. 'a * 'b -> real
しかし以下add3のようにcase文のマッチが不十分だと,型が書けませんね.
int * intとreal * realのみ許すという不思議なものになってしまっています.
val add3 = case 'a in 'a * 'b -> real of int => (case 'b in 'a * 'b -> real of int => addIntInt) | real => (case 'b in 'a * 'b -> real of real => addRealReal)
よくよく考えるとこれらの関数add2やadd3は便利そうだけど,文法で許されても型がおかしいものが書けてしまいます.
そこで複数の型パラメータに対して,別々なオーバーロードを定義してみましょう.
(* overload2.smi やっぱりダメな例(`・∀・´;) *) _require "basis.smi" val addIntInt : int * int -> real val addIntReal : int * real -> real val addRealInt : real * int -> real val addRealReal : real * real -> real val addInt = case 'b in int * 'b -> real of int => addIntInt | real => addIntReal val addReal = case 'b in real * 'b -> real of int => addRealInt | real => addRealReal val add = case 'a in 'a * 'b -> real of int => addInt | real => addReal
$ smlsharp -c overload2.sml $ smlsharp -c main.sml overload.smi:17.4-20.20 Error: (name evaluation EI-020) invalid overload instance: addInt overload.smi:17.4-20.20 Error: (name evaluation EI-020) invalid overload instance: addReal
ぐぬぬぬ.
val addInt : 'b::{int,real}. int * 'b->real val addReal : 'b::{int,real}. real * 'b->real
の2つが合成されて,
val add : 'a::{int,real}, 'b::{int,real}. 'a * 'b -> real
な型になるのを期待したけどダメか….
overload.smlのコンパイルは通ってしまいました.
ちょっと期待したけど,しかしmain.smlのコンパイルで弾かれます.
この挙動の理由はわかりません.
結局,意図していた任意の型の組み合わせで使えるaddは定義できませんでした.
残念(´・ω・`)
というわけで今日はオーバーロードの説明とその制限についてを簡単に説明しました.
残念ながら自分にはオーバーロードをうまく使いこなせませんでした.
今回は俺俺オーバーロード関数をコンパイルモードで使う方法を対象にしました.
REPLで使う方法はまた次回にでも.
*1:http://d.hatena.ne.jp/eldesh/20120416/1334588973
*2:開発陣の論文が言ってた!https://www.jstage.jst.go.jp/article/jssst/29/1/29_1_1_191/_pdf
*3:"多相レコードを持つ言語なら容易に実装できる"って多相レコード持ってる言語ってそりゃ(モゴモゴ