Crystal 言語の型と制御構文
Rubyに似た構文で記述可能で、実行速度と安全性を兼ね備えたCrystal言語。
この言語には、安全性故にRubyではあまり意識しない"型"という概念が重要になります。
ここでは、そんなCrystal言語の制御構文による型の変化について記載します。
型の変化とは
型の変化とはCrystal言語内でユニオン型※と呼ばれる変数の型に発生します。
ユニオン型がかかわる制御構文によって、変数の型が変化していくことを指しています。
※ここではバーチャル型も併せてユニオン型と記載しています。
ユニオン型とは
ユニオン型について具体的な例を見てみましょう。
以下の例は受け取った文字が文字列の何文字目に存在しているかを返すString.indexメソッドの公式リファレンスの使用例です。
"Hello, World".index('o') # => 4
"Hello, World".index('Z') # => nil
このメソッドは文字列の中に引数の文字が含まれていればインデックスを返し、
存在しない場合はnilを返します。
この時、このメソッドが返す値はInt32かNil型の値となります。
ではこのメソッドからの返り値を入れる変数の型はどうなるでしょうか。
ここで登場するのがユニオン型です。
index : Int32 | Nil # ※
※これはInt32?とも書くことができます。型名 + ? は 型 | Nil の糖衣構文です。
ユニオン型は複数の型を表現することができるため非常に便利ですが、
その反面変数の中に実際に入っている値の型がわかりません。
index = str.index('/')
index #この時点で変数indexは変数strの内容によってInt32かNil型のどちらかの値が入っている。
上記の例では2行目の時点で変数indexにはInt32型の整数値かnilのどちらかが入っています。
では変数indexを実際に使用しようとしたとき、どのように扱うべきでしょうか。
例えば、文字列strのなかの'/'の次の文字を確認したいときは以下のようなコードになると思います。
index = str.index('/')
p str[index + 1] # => コンパイルエラー
もちろん上記はコンパイルエラーとなります。
なぜなら変数indexは足し算ができるInt32型ではなくInt32とNilのユニオン型だからです。
この時点で変数indexはnilの可能性もあるのです。
ではこの変数indexは今後のコードで計算やメソッド呼び出しが行えないのでしょうか。
そんなことはありません。ここで登場するのが型の変化です。
crystalコンパイラはif文の条件内に型を制限する記述があるとき、そのif文内では変数の型を条件に沿った型に変化させてくれます。
if(<変数に対して型を制限する条件式>)
#条件式の変数は制限された型となる
else
#条件式の変数は制限された型以外となる
end
nil?
実際に型の変化を見てみましょう。
p typeof(index) # => (Int32 | Nil)
if(index.nil?)
p typeof(index) #1 => Nil
else
p typeof(index) #2 => Int32
end
上記のif文の条件分岐にnil?メソッドを使用しました。
このメソッドはインスタンスがnilの時にtrueを返します。
すなわち、この条件式を評価したときifブロック内#1へ到達するのは変数indexがnilの時のみとなります。
よってコンパイラはこのブロック内では変数indexをユニオン型ではなくNil型として扱います。
また、elseブロックに到達するのは変数indexがnilでないときのみです。
そのため、#2の時には変数indexはInt32型として扱われます。
上記の例は以下のように書きなおすこともできます。
p typeof(index)
if(index)
p typeof(index) #2 => Int32
else
p typeof(index) #1 => Nil
end
crystal言語ではnilを評価するとfalseとして評価されるため、
#2に到達できるのはnil以外となるためです。
is_a?
上記の例ではNilかそれ以外を分類できましたが、逆にNil以外の型が複数ある場合には型の制限ができません。
もし以下のような例の場合、変数valueの型を制限するためにはどうするべきでしょうか。
def return_int_or_str
#Int32かString型の値を返す関数
value = Random.rand(100)
return value % 3 == 0 ? "fizz" : value
end
value : Int32 | String
value = return_int_or_str
この時のベストプラクティスは引数の型のインスタンスかどうかを調べるis_a?メソッドを使用することでしょう。
p typeof(value) # => (Int32 | String)
if value.is_a?(Int32)
p typeof(value) #1 => Int32
else
p typeof(value) #2 => String
end
is_a?メソッドはインスタンスの型が引数で受け取った型、もしくはサブクラスの型のときにtrueを返します。
#1に到達するのは変数valueがInt32型の時のみになるため、#1ではvalueはInt32型として利用することができます。
また、#2に到達するのはvalueがInt32型以外の為、上記の例ではString型となります。
型変化の例外
ここまで記載してきた型変化についてですが、クラス変数とインスタンス変数については例外です。
クラス変数とインスタンス変数については条件式を通過しても型は一定のままとなります。
クラス変数やインスタンス変数の型を変化させるためには一度メソッド内のローカル変数へ代入を行う必要があります。
class Sample
@value : Int32?
def value
if(@value)
typeof(@value) # => (Int32 | Nil)
#インスタンス変数、クラス変数は型制限が機能しないため、ここでもユニオン型として扱われる。
end
value = @value
if(value)
typeof(value) # => Int32
end
end
end