タプルとは、要素を順番に並べた有限のリストで、イミュータブルです。 タプルでは、すべての位置が固定された型を持っている必要があります。 そのため、コンパイラーは各位置にどの型があるかを把握しています。 タプルで使われる型は位置ごとに異なっていてもかまいませんが、それらの型はコンパイル時に判明している必要があります。
タプルの値の型をコンパイル時に解釈できるかどうかに応じて、タプルの作成方法は異なります。 値がコンパイル時に判明している場合は、タプルリテラルの構文でタプルを作成できます。そうでない場合は、明示的に宣言する必要があります。 また、値の型がタプルで指定した型と一致していること、そして値の個数が指定した型の個数と一致していることも重要です。 タプルリテラルの構文で定義する例を次に示します。
tuple = {1, "foo", 'c'} # Tuple(Int32, String, Char)
Tupleクラスを使ってタプルを作成することもできます。
tuple = Tuple(Int32, String, Char).new(1, "foo", 'c')
あるいは、タプルを代入する変数の型を明示的に指定することもできます。
tuple : Tuple(Int32, String, Char) = {1, "foo", 'c'}
タプルの型を明示的に指定すると、ある位置がユニオン型を保持するように定義できるため、便利なことがあります。 つまり、1つの位置に複数の型を入れられるということです。
tuple : Tuple(Int32 | String, String, Char) = {1, "foo", 'c'}
Tupleクラスのfromメソッドを使うと、配列からタプルを作成できます。
そのためには、タプルの型を指定する必要があります。
array = [1, "foo", 'c']
tuple = Tuple(Int32, String, Char).from(array)
to_aメソッドを使うと、タプルを配列に変換できます。
変換後の配列の要素の型は、タプルの各要素の型のユニオンになります。
tuple = {1, "foo", 'c'}
array = tuple.to_a
array # => [1, "foo", 'c']
配列と同じく、タプルのインデックスは0から始まります。つまり、最初の要素のインデックスは0です。
ただし配列と違い、各要素の型は固定されていてコンパイル時に判明しています。そのため、タプルのインデックスを指定したとき、要素の型はその位置に固有のものになります。
タプルの要素にアクセスするには、[]演算子を使います。
array = [1, "foo", 'c']
array[0] # => 1
typeof(array[0]) # => Int32 | String | Char
tuple = {1, "foo", 'c'}
tuple[0] # => 1
typeof(tuple[0]) # => Int32
配列から要素にアクセスする場合とのもう1つの違いは、インデックスを指定すると、コンパイラーがそのインデックスがタプルの範囲内にあるかどうかをチェックすることです。 つまり、実行時エラーではなくコンパイル時エラーになります。
tuple = {1, "foo", 'c'}
tuple[3]
# => Error: index out of bounds for Tuple(Int32, String, Char) (3 not in -3..2)
ただし、インデックスが変数に格納されている場合、コンパイラーはコンパイル時にそのインデックスがタプルの範囲内にあるかどうかを確認できないため、代わりに実行時エラーになります。
[]演算子に範囲を指定すると、タプルの部分タプルを取得できます。
返されるのは、指定した範囲の要素を持つ新しいタプルです。
範囲はコンパイル時に与える必要があります。そうでないと、コンパイラーは部分タプルの要素の型を把握できません。
つまり、範囲は範囲リテラルでなければならず、変数に代入してはいけません。
tuple = {1, "foo", 'c'}
subtuple = tuple[0..1] # Tuple(Int32, String)
i = 0..1
tuple[i]
# Error: Tuple#[](Range) can only be called with range literals known at compile-time
決まった個数の値をまとめたいときで、しかも値の型がコンパイル時に判明している場合に、タプルは役立ちます。 なぜなら、タプルはイミュータブルであるため、配列よりもメモリを消費せず、高速だからです。 もう1つの用途は、メソッドから複数の値を返すことです。 値の型が異なる場合に特に便利です。タプルの位置ごとに異なる型を持てるからです。
サイズが大きくなったり小さくなったりするデータ構造が必要な場合や、頻繁に変更する必要がある場合には、タプルを使うべきではありません。
シンボルとは、値を参照するために使える名前付きの識別子です。
シンボルはシンボルリテラルで作成します。名前の前に:を付ける形で、たとえば:fooのようになります。
引用符で囲んで書くこともでき、たとえば:"foo"のように書きます。これにより、たとえば名前に空白を入れられます。
:foo # => :foo
:"foo boo" # => :"foo boo"
:foo == :"foo" # => true
シンボルは、名前付きタプルのキーや、メソッドや変数の名前を表すためなど、言語のさまざまな場所で使われます。
Crystalのシンボルは、Rubyのシンボルとはかなり異なります。 Crystalでは、シンボルは定数の一種であり、そのためコンパイル時に割り当てられます。 つまり、Rubyでは可能なシンボルの動的な作成が、Crystalではできません。
CrystalのシンボルはInt32として表現されるため、非常に効率的です。
シンボルは文字列に変換できますが、その逆はできません。 シンボルはコンパイル時に作成され、文字列は実行時に作成されるからです。
:foo.to_s # => "foo"
友達にクッキーを焼いてあげようと準備していると、レシピで使われているいくつかの計量単位を変換しなければならないことに気づきました。 メートル法にしか馴染みがないので、自分が使いやすいように、アメリカでよく使われるお菓子作りの計量をミリリットル(mL)に変換する方法を考える必要があります。
解答には、次の変換表を使いましょう。
| 変換する単位 | 体積 | ミリリットル(mL) |
|---|---|---|
| mL | 1 | 1 |
| 米国カップ | 1 | 240 |
| 米国液量オンス | 1 | 30 |
| 米国小さじ | 1 | 5 |
| 米国大さじ | 1 | 15 |
優秀なプログラマーを目指して腕を磨いているなら、ここに挙げたどの単位からどの単位へでも(同じ単位へでも)変換しやすくするために、ミリリットルを中間の単位として使うのがよいでしょう。
KitchenCalculator.get_volumeメソッドを実装しましょう。
体積ペアのタプルが与えられたら、数値の部分だけを返します。
KitchenCalculator.get_volume({:cup, 2.0})
# => 2.0
KitchenCalculator.to_milliliterメソッドを実装しましょう。
体積ペアのタプルが与えられたら、変換表を使って体積をミリリットルに変換します。
KitchenCalculator.to_milliliter({:cup, 2.5})
# => {:milliliter, 600.0}
KitchenCalculator.from_milliliterメソッドを実装しましょう。
体積ペアのタプルと変換したい単位が与えられたら、変換表を使って、その単位に体積を変換します。
KitchenCalculator.from_milliliter({:milliliter, 1320.0}, :cup)
# => {:cup, 5.5}
KitchenCalculator.convertメソッドを実装しましょう。
体積ペアのタプルと変換したい単位が与えられたら、与えられた体積をその単位に変換します。
KitchenCalculator.convert({:teaspoon, 9.0}, :tablespoon)
# => {:tablespoon, 3.0}