関数 print は文字列を標準出力(画面)へ出力する関数です。次の例を見てください。
- print "hello, world\n"; hello, world val it = () : unit
print は画面に文字列を出力する「副作用 (side effect)」が目的の関数です。hello, world は出力結果であり、print の返り値ではありません。() が print の返り値で、これをユニット (unit) といいます。unit 型のデータは () しかなく、print のように副作用が目的の関数の返り値などで使用されます。
SML/NJ の print は文字列しか表示できないので、他のデータ型を表示するにはそれを文字列に変換する必要があります。たとえば、整数や実数は次に示す関数で変換することができます。
- Int.toString 100; val it = "100" : string - Real.toString 1.234; val it = "1.234" : string
SML/NJ には「ストラクチャ (structure)」というモジュール機能があります。ストラクチャはデータ型の定義やそれを操作する関数などをまとめたものです。Int はストラクチャの名前で、この中に整数を操作する関数が定義されています。ストラクチャ内の関数を呼び出すには、ストラクチャ名と関数名をドット ( . ) でつなげます。
structure_name.function_name
関数名が同じでもストラクチャ名が異なれば、別々の関数として識別されます。Int.toString は Int で定義されていて、Real.toString は Real で定義されています。関数名は同じ toString でも異なる関数になります。ストラクチャは SML/NJ の重要な機能なので、あとで詳しく説明します。
リストを表示する場合は高階関数を使うと便利です。つまり、副作用を伴う関数を受け取り、その関数をリストの要素に適用するのです。SML/NJ には app という高階関数が用意されていますが、私達でも簡単に作ることができます。関数名は Common Lisp から拝借して mapc としました。
リスト : mapc fun mapc(_, nil) = () | mapc(f, x::xs) = (f x; mapc(f, xs))
最初の定義が空リストの場合です。mapc は関数 f の副作用が目的なので、返り値はユニットにします。副作用を伴う関数を使う場合、関数定義で複数の式を書く必要があります。mapc は要素 x に関数 f を適用してから mapc を再帰呼び出しします。SML/NJ で複数の式を書くには複文という機能を使います。複文は複数の式をカッコで囲んで、式をセミコロン ( ; ) で区切ります。
複文 : ( expr1; expr2; .....; exprN )
実は、let の in ... end の部分も複文になっていて、セミコロンで区切れば複数の式を書くことができます。
それでは、簡単な実行例を示します。
- mapc(fn x =>print(Int.toString(x) ^ "\n"), [1,2,3,4,5]); 1 2 3 4 5 val it = () : unit
SML/NJ の入出力は Common Lisp と同様に「ストリーム (stream)」を介して行われます。入力ストリームを表すデータ型が instream で、出力ストリームを表すデータ型が outstream です。テキストファイルの入出力はストラクチャ TextIO にまとめられています。ファイル入出力の詳細はあとで詳しく説明するとして、ここでは標準入出力について簡単に説明します。
標準入出力に対応するストリームは TextIO 内の変数に定義されています。
print は文字列を標準出力へ出力する関数でしたが、入力ストリームから文字列を読み込む関数が inputLine です。
- TextIO.inputLine TextIO.stdIn; hello, world val it = SOME "hello, world\n" : string option
hello, world と入力してリターンキーを押すと、inputLine は入力データを文字列にして SOME に包んで返します。このとき、文字列に改行文字が含まれることに注意してください。
それでは簡単な例題として、入力をそのままエコーバックする関数 echo を作ってみましょう。プログラムは次のようになります。
リスト : echo fun echo () = let val x = TextIO.inputLine TextIO.stdIn in if isSome x then (print(valOf x); echo()) else () end
echo のように副作用が目的の関数で、引数が必要ない場合はユニット () を渡します。inputLine で標準入力から 1 行読み込み、その値を変数 x に束縛します。入力データが無い場合、inputLine は NONE を返します。Windows の場合、標準入力から Ctrl-Z を入力すると、inputLine の返り値は NONE になります。isSome で x が SOME かチェックします。そうであれば、print で valOf x を画面に出力して echo を再帰呼び出しします。
簡単な実行例を示します。
val echo = fn : unit -> unit - echo(); abcd <-- 入力 abcd efgh <-- 入力 efgh hello, world <-- 入力 hello, world
Windows で echo を終了するには Ctrl-Z を入力してください。
最後に、素数を求めるプログラムを作ってみましょう。いちばん簡単な方法は、奇数 3, 5, 7, 9, ... をそれまでに見つけた素数で割ってみることです。素数はリストに格納しておけばいいでしょう。このとき、素数を全部チェックする必要はありません。実は、√n よりも小さい素数を調べるだけで、n が素数か判定することができます。
プログラムは次のようになります。
リスト : 素数を求める fun is_prime(_, nil) = true | is_prime(n, p::ps) = if p * p > n then true else if n mod p = 0 then false else is_prime(n, ps) fun prime_sub(n, m, ps) = if n > m then ps else prime_sub(n + 2, m, if is_prime(n, ps) then ps @ [n] else ps) fun primes n = prime_sub(3, n, [2])
関数 primes n は n 以下の素数をリストに格納して返します。実際の処理は関数 prime_sub で行います。第 1 引数 n がチェックする整数、第 2 引数 m が上限値、第 3 引数 ps が素数のリストです。奇数だけチェックすればいいので、n の初期値は 3 とし、値を +2 ずつしていきます。素数のリストは [2] に初期化します。prime_sub は n > m になったら再帰呼び出しを終了し、素数のリスト ps を返します。そうでなければ、関数 is_prime で n が素数かチェックします。そうであれば、ps @ [n] で素数のリストの最後に n を追加します。
素数を判定する is_prime は簡単です。第 1 引数 n がチェックする整数で、第 2 引数が素数のリストです。最初の定義が空リストの場合です。次の定義でリストを p::ps に分解し、n が p で割り切れるかチェックします。p > √n のチェックを p * p > n で行っていることに注意してください。そうであれば、n は素数なので true を返します。n mod p = 0 ならば、n は素数ではないので false を返します。それ以外の場合は is_prime を再帰呼び出しして、次の素数を調べます。
それでは実行してみましょう。
- primes 100; val it = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,...] : int list
SML の対話モードは、長いリストの要素をすべて表示しないのがデフォルトの動作なので、int list を表示する関数を作りましょう。次のリストを見てください。
リスト : int list の表示 fun print_intlist nil = print "\n" | print_intlist (x::xs) = ( print(Int.toString x); print " "; print_intlist xs ) (* 別解 *) fun print_intlist xs = mapc(fn x => (print(Int.toString x); print " "), xs)
print_intlist は引数のリストを x::xs で分解し、Int.toString x で x を文字列に変換し、それを print で出力します。別解のように、高階関数 mapc を使ってもよいでしょう。
それでは実行してみましょう。
- print_intlist(primes 100); 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 val it = () : unit - print_intlist(primes 1000); 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 457 461 463 467 479 487 491 499 503 509 521 523 541 547 557 563 569 571 577 587 593 599 601 607 613 617 619 631 641 643 647 653 659 661 673 677 683 691 701 709 719 727 733 739 743 751 757 761 769 773 787 797 809 811 821 823 827 829 839 853 857 859 863 877 881 883 887 907 911 919 929 937 941 947 953 967 971 977 983 991 997 val it = () : unit
正常に動作していますね。ただし、演算子 @ を使ってリストの末尾に素数を追加しているので、その部分でかなりの無駄があります。リスト以外のデータ構造、たとえば「配列 (array)」を使うと、もう少し速くなると思います。