M.Hiroi's Home Page

お気楽 Standard ML of New Jersey 入門

レコード

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

はじめに

今まで、新しいデータ型を定義する方法として、組と datatype を説明しました。今回は「レコード (record)」を説明します。実をいうと、レコードは SML/NJ の基本的なデータ型の一つで、今まで使ってきた「組」はレコードの特殊な場合なのです。

●レコードの定義

レコードは中カッコ { } で囲み、要素をカンマで区切ります。要素は次の形式で定義します。

ラベル = 値

ラベルは数字または名前です。"ラベル = 値" を「フィールド」、ラベルを「フィールド名」と呼ぶ場合があります。値の部分に式を書いてもかまいません。式の評価結果がラベルの値になります。次の例を見てください。

- {foo=10, bar=20};
val it = {bar=20,foo=10} : {bar:int, foo:int}
- {baz=100, 5=200};
val it = {5=200,baz=100} : {5:int, baz:int}

- {1=10, 2=20, 3=30};
val it = (10,20,30) : int * int * int

- {1=10, 2=20, 4=40};
val it = {1=10,2=20,4=40} : {1:int, 2:int, 4:int}

レコードの型は {ラベル1 : 型式, ラベル2 : 型式, ...} で表されます。{foo=10, bar=20} の型は {bar:int, foo:int} になります。要素の順番は関係ありません。{bar:int, foo:int} も {foo:int, bar:int} も同じ型になります。SML/NJ は要素を昇順に並べて表示します。

組はラベルが 1 から n までの整数で表されている特別な場合です。したがって、レコード {1=10, 2=20, 3=30} は組 (10, 20, 30) と同じです。{1=10, 2=20, 4=40} は 3 が抜けているので組にはなりません。

SML/NJ の場合、コロン ( : ) を使ってデータ型を指定することができます。次の例を見てください。

- fun square x = x * x;
val square = fn : int -> int
- fun square1 x : real = x * x;
val square1 = fn : real -> real

- val a = nil;
val a = [] : 'a list
- val b = nil : int list;
val b = [] : int list

関数 square の型は int -> int になりますが、引数 x の型を指定すると real -> real になります。また、空リスト (nil) の型は 'a list ですが、特定の型を指定することもできます。

レコードの値を参照するには #ラベル を使います。

- val a = {foo=10, bar=20};
val a = {bar=20,foo=10} : {bar:int, foo:int}
- #foo a;
val it = 10 : int
- #bar a ;
val it = 20 : int

レコードに #ラベル を適用すると、ラベルの値を参照することができます。組は番号がラベルのレコードなので #番号 で値を参照できるわけです。

●レコードのパターン

もちろん、レコードでもパターンを使うことができます。次の例を見てください。

- val {foo=x, bar=y} = {foo=100, bar=200};
val y = 200 : int
val x = 100 : int

- val {foo, bar} = {foo=1, bar=2};
val bar = 2 : int
val foo = 1 : int

"ラベル = パターン" とするとラベルの値とパターンがマッチングします。{foo=x, bar=y} は変数 x とラベル foo の値、変数 y とラベル bar の値がマッチングして、x = 100, y = 200 になります。パターンを省略してラベルだけ指定すると、ラベルがパターンの変数になります。{foo, bar} は変数 foo とラベル foo の値がマッチングして foo = 1 になり、変数 bar とラベル bar の値がマッチングして bar = 2 になります。

ただし、ラベルが数字の場合はパターンを省略することができません。次の例を見てください。

- val a = {1=10, bar=20};
val a = {1=10,bar=20} : {1:int, bar:int}
- val {1, bar} = a;
stdIn:23.7-23.13 Error: syntax error: deleting  COMMA ID RBRACE
- val {1=z, bar} = a;
val z = 10 : int
val bar = 20 : int

ラベルが数字の場合、パターンの指定を省略するとエラーになります。必ずパターンを指定してください。

レコードのパターンは、フィールドの指定に ... を使うことができます。... はワイルドカードで、不要なフィールドの指定を省略することができます。次の例を見てください。

- datatype foo = Bar of {abc:int, def:int, ghi:int};
datatype foo = Bar of {abc:int, def:int, ghi:int}

- val Bar{abc,...} = Bar{abc=1, def=2, ghi=3};
val abc = 1 : int

datatype で新しい型 foo を作成します。レコードはデータ型の一つなので、datatype の型式に使用することができます。データ構成子 Bar の型式は {abc:int, def:int, ghi:int} になります。このレコードからラベル abc の値を取り出したいとき、Bar {abc, ...} のように ... を指定すると、def と ghi の指定を省略することができます。

ただし、フィールド指定の省略は、レコードの型が特定できる場合にのみ可能です。単純に val {abc, ... } と指定すると、レコードの型を特定できずにエラーになります。ご注意くださいませ。

●データの個数を求める

それでは簡単な例題として、データをカウントするプログラムを作ってみましょう。たとえば、入力データが [ "a", "b", "c", "a", "c" ] であれば、"a", "b", "c" の個数を求めます。この場合、"a" が 2 個、"b" が 1 個、"c" が 2 個になります。

一番簡単な方法は連想リストを使うことです。データとその個数を組にし、それをリストに格納にします。連想リストからデータを探索し、データが見つかればその個数を +1 します。見つからなければ、新しい組を生成して連想リストに追加します。

今回はレコードの例題なので、組ではなくレコードを使いましょう。プログラムは次のようになります。

リスト : データの数え上げ

(* カウンタの定義 *)
datatype 'a counter = Cnt of {data:'a, count:int ref}

(* カウントアップ *)
fun countup(Cnt{count, ...}) = count := !count + 1

(* データの探索 *)
fun find_data(w, nil) = NONE
|   find_data(w, (R as Cnt{data, ...})::xs) =
    if w = data then SOME R else find_data(w, xs)

(* データの個数を求める *)
fun data_count(nil, result) = result
|   data_count(x::xs, result) =
    let
      val v = find_data(x, result)
    in
      if isSome v then (countup(valOf v); data_count(xs, result))  
      else data_count(xs, Cnt{data=x, count=ref 1}::result)
    end

最初に datatype で counter を定義します。データ型を限定する必要はないので、'a counter としました。レコードの型は {data:'a, count:int ref} とします。データの個数を書き換えるため、count は参照を使って int ref にします。count の書き換えは関数 countup で行います。

データの探索は関数 find_data で行います。第 1 引数の w が探索するデータ、第 2 引数がレコードを格納しているリスト (連想リスト) です。パターンでレコードの data を取り出し、w と等しいかチェックします。そうであれば、レコード R をoption 型に格納して返します。そうでなければ、find_data を再帰呼び出しして次のレコードを調べます。データが見つからなければ NONE を返します。

関数 data_count はデータの個数を数えて、データと個数を格納した連想リストを返します。第 1 引数が入力データのリストで、第 2 引数の result がレコードを格納する連想リストです。まず、find_data で連想リストからデータ x を探します。関数 isSome で option をチェックし、データがあれば関数 countup で count を +1 します。関数 valOf は option からデータを取り出す関数です。見つからなければ、レコードを生成して result に追加します。

それでは、簡単な実行例を示します。

- data_count(["a", "b", "c", "a", "c"], nil);
val it =
  [Cnt {count=ref 2,data="c"},Cnt {count=ref 1,data="b"},
   Cnt {count=ref 2,data="a"}] : string counter list

- data_count([1, 2, 3, 2, 3, 4, 3, 4, 5], nil);
val it =
  [Cnt {count=ref 1,data=5},Cnt {count=ref 2,data=4},Cnt {count=ref 3,data=3},
   Cnt {count=ref 2,data=2},Cnt {count=ref 1,data=1}] : int counter list

正常に動作していますね。ところで、このプログラムはデータの種類が増えると実行速度が遅くなります。実は、データの種類が増えると、データを探索する find_data の処理に時間がかかるのです。find_data は列の先頭から順番に要素を比較してデータを探します。これを「線形探索」といいます。

データ数が少なければ線形探索でも何とかなりますが、データ数が多くなると探索に時間がかかるようになります。このような場合、あらかじめデータを整理整頓しておくことで、特定のデータを高速に見つけることができるようになります。この代表的なアルゴリズムが「ハッシュ法」と「二分探索木」です。これらのアルゴリズムは次節以降で取り上げることにします。


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