SCM Boot Camp in Nagoya に行ってきた・・・と見せかけた SML# の多相レコードの話

SCM Boot Camp については他の方が書いてくれると思うので、違う話を書きます。
SCM Boot Camp 開始前の雑談や懇親会で話題になった SML# ですが、幸か不幸か .NET 系の言語と勘違いされがちです。
でも SML# の # は、多相レコードを扱う際に出てくる # が元になっています。
ちなみに、.NET Framework は 2000 年リリース、SML# の前身である (でいいのかな?) SML# of Kansai が 1993 年開発と言うことで、なんと .NET よりも歴史があるのです。


で、懇親会で SML# すごいよ、って話をしたら「多相レコード?何それ美味しいの?」って人がほとんどだったので、(ぶっちゃけ自分もそんな理解してないですけど) SML# の多相レコードを紹介してみようかな、と思い、このエントリを書いています。
環境は Windows を想定して、周辺環境の構築と SML# 処理系のインストールから始めるので、初心者にも安心です。たぶん。
F# か OCaml の入門レベルの知識を前提としています。
・・・おっと、こんなところにちょうどいい記事が!(ぉ
F#で初めての関数型プログラミング − @IT

SML# って何?

Standard ML という静的型付きの関数型言語を拡張した言語で、主に東北大学の大堀研究室が開発しています。
SML# の特徴としては、

  • 多相レコード
  • C 言語との連携の容易さ
  • 分割コンパイル
  • オブジェクトを移動させない GC

等があげられます。
C 言語との連携の容易さは結構衝撃的ですが、今回のゴールは多相レコードです。でも C 言語との連携の容易さにも触れます。


このエントリではどこまでが SML でどこからが SML# なのか、という区別は行いません。
「SML# では」と書かれていたとしても、実はそれ単なる SML でもできるよ、って場合もあります。

環境構築

SML# を Windows にインストールするためには、まずは MinGW と MSYS の環境を整える必要があります。
MinGW とか MSYS は SML# を使ってプログラムを書く上で主役になるわけではないので説明は省きます。
もう SML# インストール済みだよ!って方は対話環境を使ってみるからどうぞ。
Windows 以外の方は、SML#のインストール in Ch.2. SML#プログラミング環境の準備 in プログラミング言語SML#解説を参考にしてください。

MinGW のインストール

まずは MinGW をインストールしましょう。
MinGW - Minimalist GNU for Windows | Free Development software downloads at SourceForge.net
から mingw-get-inst-20111118.exe (2012/4/22 現在) をダウンロードし、インストーラを実行します。
基本的にはどんどん次に進めばいいですが、インストールするコンポーネントを選ぶ部分で一応全部にチェックを入れました。
fortran とか ObjC はさすがに入れなくても大丈夫だと思います(C++も不要っぽい?)。

MSYS のインストール

次は MSYS です。
MSYS | MinGW
のページの「MSYS 1.0.11」というリンクに移ると、そのうちインストーラのダウンロードが始まります。

これまたどんどん次に進んでいくと、入力を求められますので、以下のように入力してください。

それぞれ、「y」、「y」、「c:/mingw」です。

MSYS DTK のインストール

最後は MSYS DTK です。これはいるかどうか分かりませんが、一応インストールしました。
MSYS | MinGW
のページの「MSYS DTK 1.0」というリンクに移ると、やはりそのうちインストーラのダウンロードが始まります。

これもどんどん次へ進んで周辺環境のインストールは完了です。

設定

ユーザー環境変数に、以下の環境変数を追加してください。

変数
MINGW C:\MinGW
C_INCLUDE_PATH %MINGW%\include
CPLUS_INCLUDE_PATH %MINGW%\include
PATH %MINGW%\bin

ユーザー環境変数にすでに PATH がある場合は、その先頭に「%MINGW%\bin;」を追加してください。


次に、C:\MinGW\var\lib\mingw-get\data\profile.xml を開き、profile 要素の子要素として、

<repository uri="http://www.pllab.riec.tohoku.ac.jp/smlsharp/download/mingw32/%F.xml.lzma">
  <package-list catalogue="smlsharp-package-list"/>
</repository>

を追加してください。
MinGW環境にSML#を入れる際のxmlの例
こんな感じになります。

SML# のインストール

デスクトップにある MSYS のショートカットを起動し、以下のコマンドを実行します。
デスクトップにショートカットが無い場合、C:\msys\1.0\msys.bat から起動してください。

mingw-get update
mingw-get install smlsharp

これで、SML# の環境が整いました。

SML# のアップグレード

エントリを書いた翌日、1.0.1 がリリースされました。
このエントリの通りにインストールした方は、

mingw-get update
mingw-get upgrade smlsharp

とすれば最新版にアップグレードできます。

対話環境を使ってみる

さてそれでは SML# の対話環境を起動してみましょう。
ウィンドウを閉じてしまった人は開き直してください。

smlsharp

とすることで、SML# の対話環境が起動します。

SML# version 1.0.0 (2012-04-06 17:51:49 JST) for x86-mingw
#

と表示され、入力待ちになるはずです。
試しに、何か計算させてみます。

1 + 1;

これを入力すると、以下のような出力が得られます。

Creating library file: \s5cc./000.lib
val it = 2 : int
#

現在のバージョンの SML# は C ドライブ直下に一時ファイルの置き場を作って、そこに .lib やら .o やら .s (!) やら .so やらを出力します。
この置き場は実行毎に異なり、今回の場合は「s5cc」というフォルダが作られます (1 行目にフォルダ名が出力されている)。
対話環境を終了 (Ctrl+c) させても消えないので、自分で消すようにしましょう。


とのことなので、改善されるかもしれません。
最新の 1.0.1 で改善されました。Windows 7 では、%USERPROFILE%\AppData\Local\Temp あたりに出力されるようです。


2 行目は割とおなじみの出力ですね。
SML# では、(F# などと同様に) 評価した値を it という変数で参照できます。
つまり、先ほどの入力に続けて

it * 10;

とすると、

Creating library file: \s5cc./001.lib
val it = 20 : int

と出力され、次に使える it は 20 ということになります。
ちなみに、SML# では let ではなく val を使って変数を定義します。

val a = 10;

C 言語の関数を呼び出してみる

では SML# のすごいところの 1 つである、C 言語との連携をちょっと見てみましょう。
次のように入力してください。

val print_string = _import "printf": string -> unit;
print_string "hello, world!\n";

2 行目を実行した後に、以下のように出力されたはずです。

Creating library file: \s5cc./003.lib
hello, world!
val it = () : unit

こんな感じで、SML# では非常に簡単に C 言語の関数を呼び出すことができます。
更に・・・

(_import "printf": (string, int) -> unit)("%d\n", 42);

このように実行時にエラーさえ出なければ、割と自由に関数の型を指定できます。
printf は可変長引数を取る関数なのでまたちょっと違うんですが、C 言語の関数との対応付けは大体以下の通りです。

C 言語での関数のシグネチャ
Result func(Arg1 a, Arg2 b)
SML# 側での指定
(Arg1, Arg2) -> Result

一つ注意しなければならないのは、SML# のタプルの型は例えば「a * b」のように表すのに対して、_import での指定では「a, b」を使う点です。
これを間違ってもエラーにはなりませんが、おかしな挙動になったりするので注意してください。

タプル

タプルは、SML# で複数の値を手軽に扱いたいときに使用します。
C 言語との連携においては、タプルは構造体を扱うために使えます。
現在時刻の取得を例に見てみましょう。


C 言語で現在時刻を取得するためには、time 関数を使うらしいです。

/* 結果を入れるtime_t型(実体はlong型)の値t */
time_t t;
/* 現在時刻を取得 */
time(&t);

これを SML# で書いてみるとこうなります。

(* 渡された引数に現在時刻を表すint型の値を格納する関数 *)
val time = _import "time": int array -> unit;
(* timeの結果を入れる変数(サイズ1の配列で、要素の値は0) *)
val res = Array.array (1, 0);
(* 現在時刻を取得 *)
time res;

これで現在時刻 (エポック秒) を res の要素として格納することはできますが、これではその値が何を表しているのか直観的ではありません。
そこで、C 言語の世界ではこれを構造体に変換して扱ったりします。

/* 時刻を扱うのに便利な構造体tm */
struct tm* t2;
/* time_tをtmに変換 */
t2 = localtime(&t);
/* 現在時刻を出力 */
printf("%d:%02d:%02d\n", t2->tm_hour, t2->tm_min, t2->tm_sec);

このような、構造体を使う関数を SML# から扱うためにタプルを使います。

(* tm構造体は9個のintを持つ(C:\MinGW\include\time.h参照)ので、
   それを表すタプルをtmとして使えるようにする *)
type tm =
  int * int * int * (* 秒、分、時 *)
  int * int * int * (* 日、月、年 *)
  int * int * int;  (* 曜日、一年で何日目か、夏時間 *)
(* localtimeをSML#から使えるようにする *)
val localtime = _import "localtime": __attribute__((alloc)) (int array) -> tm;
(* int(エポック秒)をtmに変換 *)
val t = localtime res;
(* 現在時刻を出力 *)
val printf = _import "printf": (string, int, int, int) -> unit;
printf ("%d:%02d:%02d\n", (#3 t), (#2 t), (#1 t));

タプルの要素を取り出すためには、#n 関数を使います。
今回は現在時刻を出力するため、タプルの 3 番目の要素、2 番目の要素、最初の要素を取り出すために、#3 関数、#2 関数、#1 関数を使いました。


全体を書くと、

(* 結果の型 *)
type tm =
  int * int * int * (* 秒、分、時 *)
  int * int * int * (* 日、月、年 *)
  int * int * int;  (* 曜日、一年で何日目か、夏時間 *)

(* C言語の関数 *)
val time = _import "time": int array -> unit;
val localtime = _import "localtime": __attribute__((alloc)) (int array) -> tm;
val printf = _import "printf": (string, int, int, int) -> unit;

(* 現在時刻を取得 *)
val res = Array.array (1, 0);
time res;
(* int(エポック秒)をtmに変換 *)
val t = localtime res;
(* 現在時刻を出力 *)
printf ("%d:%02d:%02d\n", (#3 t), (#2 t), (#1 t));

こうなります。

注意!

とのことなので、

のように、戻り値としてではなく、最後の引数として配列でデータを受け取るのが正しい感じ?
今回はこのあたりをやるのは本題ではないので流してください。

レコード

SML# では、タプルは実はレコードとして扱うことができますし、特定のレコードはタプルとして扱うことができます。
先ほど、

type tm = int * int * ...

と、タプルの別名として tm を定義しましたが、実はこう書けます。

(* F#やOCamlは区切りに;を使うが、SML#では,を使う *)
type tm = {
  1: int, 2: int, 3: int,
  4: int, 5: int, 6: int,
  7: int, 8: int, 9: int
};

こう出力されたはずです。

type tm = int * int * int * int * int * int * int * int * int

タプルになった!
このように、SML# ではフィールド名全てが数字で構成され、それが 1 から始まる連番になっていると、タプルとして扱われるのです。
ついでに言うと、フィールドのないレコードは unit として扱えます。
各自対話環境で {}; を実行してみましょう。


タプルの n 番目の要素を取り出すために、SML# では #n を使う、と書きました。
これは実は、レコードからフィールドを取り出すための関数なのです。

(* こういうレコードの値aがあった場合に... *)
type t = { X: int, Y: int };
val a: t = { X = 10, Y = 20 };

(* #Xを使ってフィールドを取り出せる *)
print (Int.toString (#X a) ^ "\n");

タプルはフィールド名として数字を持ったレコードとして扱うことができるため、#n で n 番目の要素が取り出せたわけです。
ここら辺の統一感はなかなかに素敵ですね。


さて、上では type を使いましたが、実は SML# ではレコードを使うために type は必要ありません。

(* SML#ではfunによって関数を定義します *)
fun f { Name = n, Age = a } =
  (* 文字列の連結は^ *)
  print (n ^ ":" ^ (Int.toString a) ^ "\n");

型を明示的に定義していませんが、f には string 型の Name というフィールドと int 型の Age というフィールドを持つレコードを渡せます。

f { Name = "hoge", Age = 42 };

更に更に。SML# では好き勝手にその場でレコードを作れます。

(* 特に名前はないが、Xに10、Yに20が入ったレコードの値 *)
val a = { X = 10, Y = 20 };
(* 特に名前はないが、Xに10、Yに20、Zに30が入ったレコードの値 *)
val b = { X = 10, Y = 20, Z = 30 };
(* 特に名前はないが、Xに10、Yに20が入ったレコードの値 *)
val c = { X = 10, Y = 20 };

a = c; (* true *)
a = b; (* コンパイルエラー *)

タプル並の気軽さで、分かりやすい名前を持ったレコードが使える!

多相レコード

さてさて、やってきました多相レコード。
とりあえず、多相レコードって何?というのは置いといて、実際に例を見てもらったほうが分かりやすいでしょう。

(* Xという名前のフィールドを持つてきとーなレコードの値aとb *)
val a = { X = 42, Y = 24 };
val b = { X = "hoge" };

(* レコードrからXというフィールドの値を取り出す関数 *)
fun getX r = #X r;
val x1 = getX a;
val x2 = getX b;

a と b は X という名前のフィールドは持っていますが、その型が違います。
にもかかわらず、それらの値から X を取り出す関数というものが書けてしまうのです!!
これが多相レコード*1!!!


これだけなら「それ C++ の template でできるよ!」と言えますが・・・

// 例えばこんな感じ
#include <string>

struct type_a { int x; };
struct type_b { std::string x; };

// xというメンバがあればOK
template <class T, class X>
X get_x(T t) { return t.x; }

int main()
{
    type_a a = { 42 };
    type_b b = { "hoge" };

    int x1 = get_x<type_a, int>(a);
    std::string x2 = get_x<type_b, std::string>(b);
}

なんとこの SML# さん、多相性*2を保ったまま分割コンパイルできる*3というのです!
このあたりの仕組みはちゃんと理解してないので、より深く知りたい方は
SML# - レコード多相性の理論
などを参照してください。

まとめ

多相レコードすごい・・・!
そして SML# 素敵・・・!!


ただまぁ色々と気になるところが無いわけではありません。
で、ですね。
そういった気になるところを魔改造してしまおう!というハッカソンを構想中です。
その名も「SML# ハッカソン(仮)」!
SML# は SML# で書かれているから間違ってないもん!

参考資料

このエントリを書くにあたって参考にしたものです。ありがたや。

*1:r の型は、「X という名前のフィールドを持っているレコード」となります。他のフィールドを持っていても大丈夫です。

*2:X という名前のフィールドさえあればあとはどんな型でもいいよ、という性質

*3:多相性を保ったままオブジェクトファイルに落ちる