ししちにじゅうはち 4x7=28

よんたったー https://twitter.com/keita44_f4

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:"多相レコードを持つ言語なら容易に実装できる"って多相レコード持ってる言語ってそりゃ(モゴモゴ

*4:きっとこんなことを書いておけば,このあと開発者の方からSkypeTwitterで怒られるに違いない.