はじめの一歩

式の評価

早速 SML/NJ を立ち上げてみましょう。 端末で sml と打てば起動できます。

% sml
Standard ML of New Jersey v110.76 [built: Tue Oct 22 14:04:11 2013]

この対話環境を閉じるには,UNIX なら ^D,Windows なら ?? を入力します。 また,対話環境で実行中の処理を中断したいときには,UNIX なら ^C,Windows なら ?? を入力します。

簡単な式を評価してみましょう。

- 2 + 3;
val it = 5 : int

対話環境では,プログラムの区切りをセミコロン ; で教えることにより,式を評価できます。 セミコロンを打つまでは,複数行にわたって式を書くこともできます。

- 2
= +
= 3
= ;
val it = 5 : int

(この出力結果の2行目から4行目までの = は SML/NJ が勝手に出力したプロンプトです。 今後は見やすさのために省略します。)

2 + 3 を評価すると,val it = 5 : int という出力が得られます。 これは,式 2 + 3int 型の値 5 に評価されたことを意味しています。 式の評価結果は一時的に it という仮の変数に割り当てられます。

他にもいろいろな式を試してみましょう。

- 2;
val it = 2 : int

- it + 3;
val it = 5 : int

- 1 + 2 * 3;
val it = 7 : int

- 3.14;
val it = 3.14 : real

- not true;
val it = false : bool

- "hello";
val it = "hello" : string

- [1, 2, 3];
val it = [1,2,3] : int list

変数

既に it という名前の変数が登場しましたが,ここでは式の値に自分の好きな名前をつけてみましょう。

- val num : int = 2 + 3;
val num = 5 : int

- 2 * num;
val it = 10 : int

あるいは,型付けを型推論器に任せて,以下のように書くこともできます。

- val num = 2 + 3;
val num = 5 : int

- 2 * num;
val it = 10 : int

ここでは,5 という値に num という名前が付けられています。 手続き型の世界ではこの状況を「変数 num に値 5 を代入する」と表現しますが,関数型の世界では「変数 num を値 5束縛 (bind) する」といいます。

関数型の世界には "代入" という概念は存在しません(後述する参照型を除く)。 手続き型の世界の変数は代入によって値が変化しますが,関数型の世界の変数は値が更新されることはありません。

以下の例を見るといかにも値の更新が行われているかのように見えますが,これは単に変数 a の定義が上書きされているだけです。 (別の言い方をすれば,既に束縛されている変数の定義はこのように上書きすることが可能です。)

- val a = 123;
val a = 123 : int

- val a = "abc";
val a = "abc" : string

識別子

識別子には "英数字名" と "記号名" が使えます。

例えば,以下のようにへんてこな名前の変数を定義することもできます。

- val !? = 1;
val !? = 1 : int

予約語は以下の通りです。

abstype   and       andalso  as         case    do
datatype  else      end      exception  fn      fun
handle    if        in       infix      infixr  let
local     nonfix    of       op         open    orelse
raise     rec       then     type       val     with
withtype  while     eqtype   functor    include sharing
sig       signature struct   structure  where
(  )  [  ]  {  }  ,  :  ;  ...  _  |  =  =>  ->  #  :>

関数

簡単な関数を定義してみましょう。

- fun add (x : int) (y : int) : int = x + y;
val add = fn : int -> int -> int

- add 1 2;
val it = 3 : int

ここでも,型は以下のようにすべて省略してしまうことができます。

- fun add x y = x + y;
val add = fn : int -> int -> int

- add 1 2;
val it = 3 : int

1行目のキーワード fun で始まる宣言が,関数の宣言になっています。 ここでは,2つの引数 x, y をとり x + y の値を返す関数 add を定義しています。

関数の型は int -> int -> int のように,-> を使って表現されます。 -> の左は引数の型,右は返り値の型です。 int -> int -> int の正しい読み方については後ほど説明します。

4行目 add 1 2 では,値 12 に関数を適用 (apply) しています。 手続き型の世界での "呼び出し" という言葉の代わりに,関数型の世界では "適用" という言葉を使います。

関数適用の構文も C 言語族の構文とはずいぶん趣が異なります。 ML では,関数名に続いて引数を空白で区切って並べることにより,関数適用を記述します。 ラムダ計算の記法の影響を受けていますね。

ML では演算子が最も優先順位が低いことに注意しましょう。 例えば,add (1 * 2) (3 * 4) は括弧を外すことができません。 また,add 1 2 * add 3 4(add 1 2) * (add 3 4) のように結合します。

- add (1 * 2) (3 * 4);
val it = 14 : int

- add 1 2 * add 3 4;
val it = 21 : int

コメント

コメントは (* から *) までの間に記述します。 ネスト可能です。

(* コメントは (* ネスト可能 *) *)

ファイルの使用

プログラムをファイルに記述して,対話環境にロードすることもできます。 例として,以下のような test1.sml というファイルを作成してみましょう。

(* file: test1.sml *)
fun add x y = x + y
val m = 2
val n = 3

キーワード funval の存在でプログラムの切れ目がわかるので,対話環境では常に入力していたセミコロン (;) は必ずしもつける必要はありません。

ファイルの内容をロードするには,以下のように use 関数を使います。

- use "test1.sml";
[opening test1.sml]
val add = fn : int -> int -> int
val m = 2 : int
val n = 3 : int
val it = () : unit

- add 4 5;
val it = 9 : int

実行可能形式へのコンパイル

このサイトでは SML/NJ の対話環境しか使いませんが,SML のプログラムを実行可能ファイルにコンパイルすることも可能です。

SML/NJ をコンパイラとして使う場合は,heap2exec を用いれば実行可能ファイルを得ることができます。 ただ,この方法は少々込み入っているので,ここでは SML/NJ ではなくて MLton という処理系を使って実行可能ファイルを作成する方法を簡単に紹介します。

MLton で実行可能ファイルを作成する場合,プログラムは以下のように記述するのがよいでしょう。

(* file: test2.sml *)
fun hello name = "hello" ^ name ^ "\n"
val name1 = "ken"
val name2 = "tom"
val () = print (hello name1)
val () = print (hello name2)

基本的には val 宣言やその他の宣言の羅列でプログラムを記述します。 印字処理を行う最後の2行も val 宣言となっていますが,右辺の式の値にわざわざ名前をつける必要はないため,左辺は慣習に基づいて単に () としています( () は後述する単位型の構成子です)。

MLton を使う場合のコンパイルの仕方とプログラム実行結果は以下のようになります。

% mlton test2.sml
% ./test2
hello, ken
hello, tom