コンテナ
複数の値を保持することの出来るデータ型をコンテナと呼ぶことにします。コンテナの種類はいくつかあります。
配列
配列は同種の型の値を保持できる固定長のコンテナです。固定長であることは、作成した後にそのサイズを変更できないことを意味します。また、サイズはコンパイル時定数でなければなりません。
配列型の変数の宣言は、型を明示的に書くと次のようになります。
let xs: array[3, int] = [1, 2, 3]
echo xs #=> [1, 2, 3]
通常の変数の場合と同様に型推論が機能するので、明示的な型の指定は省略することが出来ます。
let a = [1, 2, 3]
echo a #=> [1, 2, 3]
let
で宣言する変数には初期値を与えなければなりません。var
で宣言する場合、初期値を省略するとゼロ初期化されます。
var a: array[3, int]
echo a #=> [0, 0, 0]
ゼロ値以外で初期化したい場合の特別な方法はありません。一旦変数を作成してから、for文で設定するのが妥当な方法です。
var a: array[3, int]
for x in a.mitems:
x = 99
echo a #=> [99, 99, 99]
mitems
は要素を変更可能な状態で配列を走査します。
配列の宣言にはarray[<サイズ>, <要素の型>]
ではなく、array[<インデックスの範囲>, <要素の型>]
とすることも出来ます。
var a: array[0..2, int]
echo a #=> [0, 0, 0]
これはインデックスが0から2の、すなわち、サイズが3の配列を作成します。
シーケンス
シーケンスは、配列と同様、同じ種類の型の値を保持できる可変長のコンテナです。可変長であることは、作成した後に新たな要素を追加したり削除したり出来ることを意味します。
シーケンスの変数の宣言は、型を明示的に書くと次のようになります。
let xs: seq[int] = @[1, 2, 3]
echo xs #=> @[1, 2, 3]
型の指定でarray
の代わりにseq
としていることとサイズを指定しないことが配列の場合と異なります。また、初期値として与えるシーケンスのリテラルは[<要素>]
の代わりに、@[<要素>]
となることが配列の場合と異なります。 配列の場合と同様に型推論が機能するので、明示的な型の指定は省略することが出来ます。
let xs = @[1, 2, 3]
echo xs #=> @[1, 2, 3]
上の2つの例ではシーケンスの変数の宣言にlet
を用いました。こうすると、シーケンスの内容を変更することが出来ないので意味がありません。ほとんどの場合、シーケンスはvar
にすることになるでしょう。var
にすれば、addプロシージャやdeleteプロシージャで要素の追加や削除ができます。
var xs: seq[int] = @[1, 2, 3]
xs.add(10)
echo xs #=> @[1, 2, 3, 10]
xs.delete(0)
echo xs #=> @[2, 3, 10]
deleteプロシージャの引数に与えるのは、削除したい要素がシーケンスの何番目であるかを示すインデックスです。シーケンスのインデックスは他の多くの言語と同様に0始まりです。
シーケンスを作成する別の方法は、newSeqプロシージャを使うことです。
var xs = newSeq[int]()
xs.add(1)
xs.add(2)
xs.add(3)
echo xs #=> @[1, 2, 3]
シーケンスに関連するプロシージャは他にもいくつかあります。マニュアル
インデックス
配列とシーケンスの要素には[]
とインデックスを使ってアクセスできます。
let xs = [1, 2, 3, 4, 5]
let x0 = xs[0]
echo x0 #=> 1
let x1 = xs[1]
echo x0 #=> 2
インデックスは0始まりです。範囲外アクセスは実行時にチェックされエラーとなります。
let xs = [1, 2, 3, 4, 5]
let x100 = xs[100] # 範囲外アクセス!
このプログラムを実行すると次のエラーが出力されます。
Error: index 100 not in 0 .. 4
配列は宣言でインデックスの範囲を指定できるので、そうした場合は、取り得るインデックスの範囲はその範囲に制限されます。
let xs: array[2..6, int]= [1, 2, 3, 4, 5]
let x0 = xs[0] # 範囲外アクセス!
このプログラムを実行すると次のエラーが出力されます。
Error: index 0 not in 2 .. 6
^n
で後方からインデックス出来ます。xsに対する^1
はlen(xs) - 1
、^2
はlen(xs)-2
となります。
let xs = [1, 2, 3, 4, 5]
let x4 = xs[^1]
echo x4 #=> 5
let x3 = xs[^2]
echo x3 #=> 4
後方からのインデックスのルールに従えば、xsに対する^0
はlen(xs) - 0
であり、xs[^0]
はxs[len(xs)]
ということになります。実際にそのとおりに解釈され、最後の要素を一つ越えた位置へのアクセスになるので、実行時に範囲外アクセスエラーとなります。
配列やシーケンスをlet
ではなくvar
で宣言すれば、インデックスの位置の要素を変更することが出来ます。
var xs =[1, 2, 3, 4, 5]
xs[0] = 100
xs[1] = 200
xs[^2] = 400
echo xs #=> [100, 200, 3, 400, 5]
インデックスによるアクセスはstring
でも同じように使えます。
スライス
[]
で指定するインデックスを範囲指定に置き換えると、スライスを取得することが出来ます。
let xs = [1, 2, 3, 4, 5]
let s = xs[1..3]
echo s #=> @[2, 3, 4]
出力に@
が付加されていることから、スライスはシーケンスであることが見て取れます。スライスをlet
ではなくvar
とすることで、変更を加えることが出来ます。この場合、s
に変更を加えても元となった配列に影響は及びません。
let xs = [1, 2, 3, 4, 5]
var s = xs[1..3]
s[0] = 200
s.add(999)
echo s #=> @[200, 3, 4, 999]
echo xs #=> [1, 2, 3, 4, 5]
s
に対する変更がxs
に反映されていないことが分かります。しかし、取得したスライスを変数に格納することなく、直接に値を代入することで、元になった配列の複数要素へ変更を加えることが出来ます。
var xs = [1, 2, 3, 4, 5]
xs[1..3] = [200, 300, 400] # @[200, 300, 400]としても同じ結果になる
echo xs #=> [1, 200, 300, 400, 5]
スライスへの代入の挙動には、配列とシーケンスで重要な違いがあります。配列の場合はxs[1..3]
の要素数が3つなので、右辺の要素数もちょうど3つでなければなりません。もしそうでなければ、実行時エラーとなります。
var xs = [1, 2, 3, 4, 5]
xs[1..3] = [200, 300, 400, 500] # 要素数が異なる!
このプログラムを実行すると、次のエラーが出力されます。
Error: unhandled exception: different lengths for slice assignment [RangeDefect]
シーケンスの場合は問題ありません。スライスの範囲が右辺の要素数に合うように置き換えられます。
var xs = @[1, 2, 3, 4, 5]
xs[1..3] = [200, 300, 400, 500]
echo xs #=> @[1, 200, 300, 400, 500, 5]
xs[1..3] = [999]
echo xs #=> @[1, 999, 500, 5]
スライスはstring
にも使えます。変更を加える場合は、シーケンスと同じように、スライスの範囲が右辺の要素数に合うように置き換えられます。
タプル
タプルは異なる種類の型の値を保持できる固定長のコンテナです。タプルの変数は次のように宣言することが出来ます。
let one: (int, float, string) = (1, 1.0, "one")
echo one #=> (1, 1.0, "one")
これまでと同様に型推論が機能するので、明示的な型の指定は省略することが出来ます。
let one = (1, 1.0, "one")
echo one #=> (1, 1.0, "one")
タプルの各要素にアクセスするには[]
とインデックスを使います。
let one = (1, 1.0, "one")
let i = one[0]
let f = one[1]
let s = one[2]
echo i #=> 1
echo f #=> 1.0
echo s #=> one
タプルをlet
ではなくvar
で宣言すれば、要素に変更を加えることも出来ます。
タプルは要素を指すインデックスに名前を付けることが出来ます。これをフィールド名と呼ぶことにします。
let one: tuple[ival: int, fval: float, name: string] = (ival: 1, fval: 1.0, name: "one")
echo one #=> (ival: 1, fval: 1.0, name: "one")
やはり、型推論によって明示的な型の指定は省略できます。
let one = (ival: 1, fval: 1.0, name: "one")
echo one #=> (ival: 1, fval: 1.0, name: "one")
各フィールドにアクセスするには.フィールド名
とします。
let one = (ival: 1, fval: 1.0, name: "one")
let i = one.ival
let f = one.fval
let s = one.name
echo i #=> 1
echo f #=> 1.0
echo s #=> one
タプルをlet
ではなくvar
で宣言すれば、フィールドの値に変更を加えることも出来ます。
その他のコンテナ
これまで見てきたコンテナは、シーケンシャルに順序付けられたコンテナでした。その他のコンテナについても少しだけ触れておきます。
キーと値のペアを格納するコンテナはテーブル(table)という名前で提供されています1)。テーブルを利用するには、import std/tables
として、標準ライブラリのtablesモジュールをインポートする必要があります。
数学の集合を表すsetが組み込みでサポートされていますが、上記のテーブルの値を持たないバージョンのようなものではなく、もっと限定的なものです。他の言語のsetような用途の広いsetが必要ならば、import std/sets
として、標準ライブラリのsetsモジュールをインポートする必要があります。このモジュールによりHashSetが利用できるようになります。