M.Hiroi's Home Page

お気楽 Standard ML of New Jersey 入門

配列と参照

Copyright (C) 2005-2020 Makoto Hiroi
All rights reserved.

はじめに

SML/NJ は関数型言語なので、関数の引数や let で定義した局所変数の値を書き換えることはできません。ところが、「配列 (array)」と「参照 (reference)」というデータ型に限り、値を書き換えることができます。つまり、手続き型言語のように「副作用」を伴う操作を行うことができるわけです。関数型言語としては不純な機能なのですが、問題によっては配列や参照を使った方が簡単にプログラムできる場合もあります。

●配列

配列を操作する関数はストラクチャ Array に定義されています。配列の生成は関数 array を使います。

Array.array(size, init_value)

array で生成される配列は 1 次元配列です。size は配列の大きさ、init_value は初期値を指定します。簡単な例を示します。

- val a = Array.array(8, 0);
val a = [|0,0,0,0,0,0,0,0|] : int array

- val b = Array.array(8, true);
val b = [|true,true,true,true,true,true,true,true|] : bool array

- val c = Array.array(8, (0, 0));
val c = [|(0,0),(0,0),(0,0),(0,0),(0,0),(0,0),(0,0),(0,0)|] : (int * int) array

SML/NJ の場合、配列は [| ... |] で表します。ただし、リストや組と違って配列の生成に [| ... |] を使うことはできません。配列の型はリストのように格納するデータ型によって決まります。int を格納する配列は int array に、bool であれば bool array に、組であれば (int * int) array のようになります。

配列のアクセスは関数 sub と update で行います。

Array.sub(a, n)
Ayyay.update(a, n, data)

関数 sub は第 1 引数に配列、第 2 引数に整数値 (添字 : subscript) を指定します。配列の要素は 0 から数えます。関数 sub をC言語で書くと a[n] に相当します。次の例を見てください。

- a;
val it = [|0,0,0,0,0,0,0,0|] : int array
- Array.sub(a, 0);
val it = 0 : int
- Array.sub(a, 7);
val it = 0 : int
- Array.sub(a, 8);

uncaught exception Subscript [subscript out of bounds]

配列 a の大きさは 8 なので、添字の範囲は 0 から 7 になります。範囲外の添字を指定すると例外が送出されます。

配列の値を書き換えるには関数 update を使います。第 1 引数が配列、第 2 引数が添字、第 3 引数が配列に代入するデータです。関数 update をC言語で書くと a[n] = data に相当します。次の例を見てください。

- a;
val it = [|0,0,0,0,0,0,0,0|] : int array
- Array.update(a, 0, 10);
val it = () : unit
- Array.update(a, 7, 17);
val it = () : unit
- a;
val it = [|10,0,0,0,0,0,0,17|] : int array

このように、update は配列 a の値を書き換えます。update の返り値は unit です。

●配列の操作関数

ここで、ストラクチャ Array に用意されている関数をいくつか紹介しましょう。配列の大きさは関数 length で求めることができます。

- a;
val it = [|10,0,0,0,0,0,0,17|] : int array
- Array.length a;
val it = 8 : int

関数 fromList はリストを配列に変換します。

- Array.fromList([1,2,3,4,5]);
val it = [|1,2,3,4,5|] : int array

- Array.fromList(["ab","cd","ef","gh"]);
val it = [|"ab","cd","ef","gh"|] : string array

Array には配列用の高階関数も用意されています。

val app : ('a -> unit) -> 'a array -> unit
val foldl : ('a * 'b -> 'b) -> 'b -> 'a array -> 'b
val foldr : ('a * 'b -> 'b) -> 'b -> 'a array -> 'b
val modify : ('a -> 'a) -> 'a array -> unit
val tabulate : int * (int -> 'a) -> 'a array

app, foldl, foldr は今までに説明した高階関数の配列バージョンです。関数 modify は配列の要素に関数を適用し、その結果を配列に代入します。配列の内容が書き換えられることに注意してください。つまり、結果を格納する配列を新しく生成するのではなく、引数の配列に結果を代入するのです。簡単な例を示します。

- val a = Array.fromList [1,2,3,4,5];
val a = [|1,2,3,4,5|] : int array

- Array.modify (fn x => x * x) a;
val it = () : unit
- a;
val it = [|1,4,9,16,25|] : int array

このように、modify は引数 a の配列を直接書き換えています。

関数 tabulate は整数 n と関数 f を受け取り、0 から n - 1 までの値を関数 f に適用して、その結果を格納した配列を生成します。次の例を見てください。

- Array.tabulate(8, fn(x) => x);
val it = [|0,1,2,3,4,5,6,7|] : int array

- Array.tabulate(8, fn(x) => x * 2);
val it = [|0,2,4,6,8,10,12,14|] : int array

ちなみに、tabulate は配列だけではありません。ストラクチャ List にはリストを生成する tabulate が用意されています。

●参照

「参照 (reference)」はデータを間接的に参照する機能です。一般に、変数束縛は変数とデータを直接結び付けます。これに対し、参照は変数とデータを直接結び付けるのではなく、その間にデータを指し示す特別なデータ型を経由します。このデータ型を「参照型」といいます。次の図を見てください。

                    データ
  ┌───┐      ┌───┐
  │変数 a│──→│  11  │
  └───┘ 束縛 └───┘

    (A) 通常の束縛

                     参照型           データ
  ┌───┐      ┌──┬─┐      ┌───┐
  │変数 b│──→│ref │・┼──→│  11  │  
  └───┘ 束縛 └──┴─┘      └───┘

    (B) 参照型データを束縛

                図 : 参照 (1)

上図 (A) は通常の変数束縛です。val a = 11 とすると、変数 a に数値 11 が束縛されます。(B) が参照を図示したものです。変数 b には参照型データが束縛され、参照型データが数値 11 を指し示しています。変数 b は参照型データを経由して数値 11 を参照することができます。

SML/NJ で参照型データを生成するには ref を使います。次の例を見てください。

- val b = ref 11;
val b = ref 11 : int ref
- b;
val it = ref 11 : int ref
- val c = ref "foo";
val c = ref "foo" : string ref
- c;
val it = ref "foo" : string ref

ref 11 は数値 11 を指し示す参照型データを生成し、それが変数 b に束縛されます。この場合、データの型は int ref になります。ref "foo" は文字列 "foo" を指し示す string ref を生成し、それが変数 c に束縛されます。今後、参照型データを束縛した変数を「ref 変数」と呼ぶことにします。

参照先のデータを求めるには演算子 ! を使います。ref 変数 b と c に演算子 ! を適用すると、次のようになります。

- !b;
val it = 11 : int
- !c;
val it = "foo" : string

- val ref d = b;
val d = 11 : int
- val ref e = c;
val e = "foo" : string

ref はパターンとしても使えるので、演算子 ! だけではなく、パターンマッチングでも参照先の値を取り出すことができます。

参照型データは参照するデータを変更することができます。次の図を見てください。

                     参照型           データ
  ┌───┐      ┌──┬─┐      ┌───┐
  │変数 b│──→│ref │・┼ X →│  11  │  
  └───┘ 束縛 └──┴┼┘      └───┘
                          │        ┌───┐
                          └───→│  22  │
                                    └───┘
    (C) データの書き換え

                図 : 参照 (2)

上図 (C) は参照するデータを数値 11 から数値 22 に変更しています。すると、ref 変数 b が参照する値は 22 になります。ようするに、ref 変数 b の値を書き換えることと同じ効果が得られるのです。ref 変数の書き換えは演算子 := で行います。次の例を見てください。

- b := 22;
val it = () : unit
- !b;
val it = 22 : int
- c := "bar";
val it = () : unit
- !c;
val it = "bar" : string

b := 22 とすると、ref 変数 b の値は 11 から 22 に書き換えられます。同様に、c := "bar" とすると、ref 変数 c の値は "foo" から "bar" になります。

●繰り返し

今まで繰り返しは再帰定義を使って行いましたが、SML/NJ には簡単な繰り返しを行う while ループが用意されています。

while expr1 do expr2

while は expr1 を評価し、その結果が真の間 expr2 を繰り返し評価します。while の処理を図に示すと次のようになります。

               ↓
               ├←─────┐ 
       偽┌─────┐      │
 ┌───│条件:expr1│      │
 │      └─────┘      │
 │            ↓真          │
 │      ┌─────┐      │
 │      │式 : expr2│      │
 │      └─────┘      │
 │            └──────┘
 └──────┐
               ↓

        図 : while の処理

図を見ればおわかりのように while はいたって単純ですが、条件が偽にならないと「無限ループ」に陥ってしまうので注意してください。while はユニットを返します。簡単な例を示しましょう。

リスト : 階乗

fun fact( n ) =
  let
    val i = ref n
    val v = ref 1
  in
    while !i > 0 do (  
      v := !v * !i;
      i := !i - 1
    );
    !v
  end

階乗を求める関数 fact を while を使ってプログラムしました。let で ref 変数 i, v を定義し、その値を書き換えながら階乗 n * (n - 1) * ... * 2 * 1 を計算しています。最後に階乗の値 !v を返します。

●FizzBuzz 問題

次は FizzBuzz 問題を SML/NJ で解いてみましょう。FizzBuzz 問題は 1 から 100 までの値を表示するとき、3 の倍数のときは Fizz を、5 の倍数ときは Buzz を表示するというものです。FizzBuzz 問題の詳細については Fizz Buzz - Wikipedia をお読みください。

プログラムは次のようになります。

リスト : FizzBuzz 問題

fun change_fizzbuzz n =
  if n mod 15 = 0 then "FizzBuzz"
  else if n mod 3 = 0 then "Fizz"
  else if n mod 5 = 0 then "Buzz"
  else Int.toString n

fun fizzbuzz () =
  let
    val n = ref 1
  in
    while !n < 100 do (
      print (change_fizzbuzz (!n));
      print " ";
      n := !n + 1
    )
  end
- fizzbuzz ();
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 31 32 Fizz 34 Buzz Fizz 37 38 Fizz Buzz 
41 Fizz 43 44 FizzBuzz 46 47 Fizz 49 Buzz Fizz 52 53 Fizz Buzz 56 Fizz 58 59 FizzBuzz 
61 62 Fizz 64 Buzz Fizz 67 68 Fizz Buzz 71 Fizz 73 74 FizzBuzz 76 77 Fizz 79 Buzz 
Fizz 82 83 Fizz Buzz 86 Fizz 88 89 FizzBuzz 91 92 Fizz 94 Buzz Fizz 97 98 Fizz 
val it = () : unit

変数 n を 1 に初期化し、while 文の中で 1 ずつ増やしていきます。この中で関数 change_fizzbuzz を呼び出して数値を文字列に変換します。最初の if 文で、n が 15 の倍数 (3 の倍数でかつ 5 の倍数) かチェックします。そうでなければ、次の else if で n が 3 の倍数かチェックし、次の else if で n が 5 の倍数かチェックします。どの条件も該当しない場合は最後の else 節で n を文字列に変換します。

なお、関数型言語らしく 1 から 100 までのリストを生成して、それをマッピングで Fizz Buzz に変換することもできます。プログラムは次のようになります。

リスト : FizzBuzz 問題 (別解)

fun iota n m =
  if n > m then nil
  else n :: iota (n + 1) m

fun fizzbuzz1 () = map change_fizzbuzz (iota 1 100)
- fizzbuzz1 ();
val it =
  ["1","2","Fizz","4","Buzz","Fizz","7","8","Fizz","Buzz","11","Fizz","13",
   "14","FizzBuzz","16",...] : string list
- app (fn x => (print x; print " ")) (fizzbuzz1 ());
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 31 32 Fizz 34 Buzz Fizz 37 38 Fizz Buzz 
41 Fizz 43 44 FizzBuzz 46 47 Fizz 49 Buzz Fizz 52 53 Fizz Buzz 56 Fizz 58 59 FizzBuzz 
61 62 Fizz 64 Buzz Fizz 67 68 Fizz Buzz 71 Fizz 73 74 FizzBuzz 76 77 Fizz 79 Buzz 
Fizz 82 83 Fizz Buzz 86 Fizz 88 89 FizzBuzz 91 92 Fizz 94 Buzz Fizz 97 98 Fizz Buzz 
val it = () : unit

●パスカルの三角形

もうひとつ簡単な例を示しましょう。「順列と組み合わせ」で作成した「パスカルの三角形」を while ループでプログラムしてみます。

リスト : パスカルの三角形を表示

fun pascal x =
  let
    val n = ref 0
    val r = ref 0
  in
    while !n <= x do (
      r := 0;
      while !r <= !n do (
        print_int( comb( !n, !r ) );  
        r := !r + 1
      );
      print("\n");
      n := !n + 1
    )
  end

ref 変数 n と r を用意し、while で「二重ループ」しています。最初のループで n の値を 0 から x まで増やし、次のループで r の値を 0 から n まで増やしています。while ループで複数の式を評価する場合は複文 (expr1; expr2; ... exprN) を使ってください。

それでは実行してみましょう。

- pascal 10;
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1
1 8 28 56 70 56 28 8 1
1 9 36 84 126 126 84 36 9 1
1 10 45 120 210 252 210 120 45 10 1
val it = () : unit

正常に動作していますね。


初版 2005 年 5 月 21 日
改訂 2020 年 8 月 9 日