はじめの一歩

1. 式の評価

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

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

この対話環境を閉じるには,UNIX であれば ^D を入力します. また,対話環境で実行中の処理を中断したいときには,UNIX であれば ^C を入力します.

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

- 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

2. 変数

既に 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

3. 識別子

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

  • 英数字名
    1字目が英字 (A-Z, a-z) またはアポストロフィ (') で,2字目以降が英字 (A-Z, a-z),数字 (0-9),アポストロフィ ('),アンダースコア (_) である文字列. (ただし,アポストロフィで始まる英数字名は型変数にのみ使われる.)

  • 記号名
    以下に示す記号からなる文字列.

! % & $ # + - / : < = > ? @ \ ~ ` ^ | *

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

- 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
(  )  [  ]  {  }  ,  :  ;  ...  _  |  =  =>  ->  #  :>

4. 関数

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

- 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

5. コメント

コメントは (* から *) までの間に記述します. ネスト可能です. ちなみに,いわゆる 1 行コメントは利用できません.

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

6. ファイルの使用

プログラムをファイルに記述して,対話環境にロードすることもできます. 例として,以下のような 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