Standard MLとは?
Standard ML(以下、SML)とは、プログラミング言語の一種です。関数型言語に分類されており、高階関数やパターンマッチングなどといった言語要素がサポートされています。
高階関数とは、引数や返り値として関数を扱う関数。高階関数は、関数を整数や文字と同様に値として扱うことができる。
パターンマッチングとは、扱うデータをパターンを用いて記述することにより、複雑な条件分岐やデータの抽出などの記述を簡略化できるしくみ。
インストール
本記事では、コンパイラはStandard ML of New Jerseyを使用します。以下のサイトからダウンロードできます。
Linuxを使用している場合は、aptコマンド等でインストールすることもできます。以下はUbuntu20.04でのインストールの例です。
sudo apt install smlnj
他にもmltonや、日本製のSML#などといったコンパイラもありますので、興味のある方は調べてみてください。
サンプルプログラムの実行
プログラミングでおなじみの、Hello World!と表示するプログラムを書いてみましょう。
インタプリタによる実行
正常にSMLがインストールされていれば、Windowsではコマンドプロンプトやパワーシェル、Linuxではbash等でsml
コマンドを実行することでインタプリタを起動できます。
$ sml
Standard ML of New Jersey v110.79 [built: Sat Oct 26 12:27:04 2019]
-
ではHello World!
と表示してみましょう。標準出力への出力はprint
関数を用います。
- val x = print "Hello World!\n";
Hello World!
val x = () : unit
print関数で文字列を出力することができました。
SMLでは、val (変数名) = (式)
で変数を宣言することができます。ここでは、変数xにprint関数の返り値が格納されます。
また、SMLでは、(関数) (引数)
の形で関数に引数を渡すことができます。この例では、print関数に文字列"Hello World!\n"
を渡しています。\n
は改行を表します。インタプリタを使用する際は、文の最後に;
をつけます。
ファイルを読み込んで実行
次に、あらかじめファイルにプログラムを記述して、インタプリタからファイルを読み込んでみましょう。以下のようなプログラムを作成します。ファイルにプログラムを記述する場合は、文の最後の;
は無くても大丈夫です。
(* hello.sml *)
val x = print "Hello World!\n"
プログラムが準備できたら、sml
コマンドでインタプリタを起動し、use (ファイル名へのパス);
でプログラムを指定して実行してみます。
- use "hello.sml";
[opening hello.sml]
Hello World!
val x = () : unit
val it = () : unit
各種文法事項
簡単に各文法事項を説明します。
(* 変数の宣言 *)
val x = 1
val str_one = "one"
(* 関数の宣言 *)
fun f x = x + 1
(* if式 *)
val y = if f 5 > 10 then "more than ten" else "ten or less"
(* 再帰呼び出し *)
fun sum n = if n <= 0 then 0 else n + sum (n - 1)
変数の宣言はval (変数名) = (式)
関数の宣言はfun (関数名) (引数) [(引数)...] = (式)
if式はif (条件) then (条件が真のときの式) else (条件が偽のときの式)
というように書きます。上の例では、関数fに5を渡した返り値は6となります。6は10より小さいので、f 5 > 10
の結果はfalseになり、elseへ分岐します。よって、変数yには文字列"ten or less"
が代入されます。
関数の中で、自分自身を呼び出す再帰呼び出しを行うこともできます。基本的に、関数型言語ではforやwhileといった言語要素を使用せず、再帰呼び出しで繰り返し処理を行います。
FizzBuzz問題
最後に、FizzBuzz問題を解いてみましょう。FizzBuzz問題とは、
「1から順に整数を出力しなさい。ただし、3の倍数のときにはFizz、5の倍数のときにはBuzz、3と5の倍数のときにはFizzBuzzを出力せよ。」
という問題です。ここでは30まで出力してみます。
解答
2つの関数を作りましょう。まず、1以上の整数nを受け取り、出力する内容を文字列として返す関数f1を次のように定義します。複数の条件が当てはまる場合には、上側の場合を優先します。例えば、引数が15のとき、15は15で割り切れますし、3でも5でも割り切れますが、一番上のFizzBuzzを出力します。
f1(x) = \left\{
\begin{array}{ll}
FizzBuzz & (xが15で割り切れるとき) \\
Fizz & (xが3で割り切れるとき) \\
Buzz & (xが5で割り切れるとき) \\
x & (その他)
\end{array}
\right.
これをプログラムにすると、次のようになります。Int.toString関数は、整数型の値を受け取って、対応するstring型の値を返す関数です。
fun f1 x =
if x mod 15 = 0 then "FizzBuzz"
else if x mod 3 = 0 then "Fizz"
else if x mod 5 = 0 then "Buzz"
else Int.toString x
次に、1から30までの出力リストを返す関数f2を定義します。f2はプログラム中で引数1を渡すものとします。SMLでは、(式1; 式2; 式3;... 式n)
と書かれた場合、式1から式nまで順に評価を行い、式nをこの式全体の評価結果とします。
fun f2 x =
if x <= 30
then (print (f1 x ^ "\n"); f2 (x + 1))
else ()
最後に、関数f2に1を渡せば、コンソール上に答えが表示されます。ちなみに、変数名をアンダースコアにすると、その値は破棄されます。
val _ = f2 1
以上でプログラムの作成が完了しました。プログラム全体は以下の通りです。「出力を分ける部分」「30まで出力を行う部分」が分かれていて見やすいですね。
fun f1 x =
if x mod 15 = 0 then "FizzBuzz"
else if x mod 3 = 0 then "Fizz"
else if x mod 5 = 0 then "Buzz"
else Int.toString x
fun f2 x =
if x <= 30
then (print (f1 x ^ "\n"); f2 (x + 1))
else ()
val _ = f2 1
インタプリタ上で実行すると次のようになります。
- use "fizzbuzz.sml";
[opening fizzbuzz.sml]
[autoloading]
[library $SMLNJ-BASIS/basis.cm is stable]
[library $SMLNJ-BASIS/(basis.cm):basis-common.cm is stable]
[autoloading done]
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
Fizz
22
23
Fizz
Buzz
26
Fizz
28
29
FizzBuzz
val f1 = fn : int -> string
val f2 = fn : int -> unit
val it = () : unit
別解
ここでは説明していませんが、map関数やapp関数、パターンマッチなどを用いると、次のように書くこともできます。
val range = List.tabulate (30, (fn x => x + 1))
val changeList = [(15, "FizzBuzz"), (3, "Fizz"), (5, "Buzz")]
fun change ((n, s)::t) x = if x mod n = 0 then s else change t x
| change nil x = Int.toString x
val _ = app (fn s => print (s ^ "\n")) (map (change changeList) range)