コンテンツへスキップ

ユニオン型

変数または式の型は、複数の型で構成される場合があります。これをユニオン型と呼びます。たとえば、異なるifブランチ内で同じ変数に代入する場合

if 1 + 2 == 3
  a = 1
else
  a = "hello"
end

a # : Int32 | String

ifの終わりでは、aInt32 | String型(「Int32とStringのユニオン」と読みます)になります。このユニオン型はコンパイラによって自動的に作成されます。実行時には、aはもちろん1つの型のみになります。これはclassメソッドを呼び出すことで確認できます。

# The runtime type
a.class # => Int32

コンパイル時の型は、typeofを使用することで確認できます。

# The compile-time type
typeof(a) # => Int32 | String

ユニオンは任意に多数の型で構成できます。型がユニオン型である式に対してメソッドを呼び出す場合、ユニオン内のすべての型がそのメソッドに応答する必要があります。そうでない場合は、コンパイル時エラーが発生します。メソッド呼び出しの型は、それらのメソッドの戻り値の型のユニオン型になります。

# to_s is defined for Int32 and String, it returns String
a.to_s # => String

a + 1 # Error, because String#+(Int32) isn't defined

必要に応じて、変数をコンパイル時にユニオン型として定義できます。

# set the compile-time type
a = 0.as(Int32 | Nil | String)
typeof(a) # => Int32 | Nil | String

ユニオン型のルール

一般に、2つの型T1T2が結合されると、結果はユニオンT1 | T2になります。ただし、結果の型が異なる型になるケースがいくつかあります。

同じ階層下にあるクラスと構造体のユニオン

T1T2が同じ階層下にあり、それらの最近傍共通祖先ParentReferenceStructIntFloatValueのいずれでもない場合、結果の型はParent+になります。これは仮想型と呼ばれ、基本的にはコンパイラが型をParentまたはそのサブタイプのいずれかとして認識することを意味します。

例:

class Foo
end

class Bar < Foo
end

class Baz < Foo
end

bar = Bar.new
baz = Baz.new

# Here foo's type will be Bar | Baz,
# but because both Bar and Baz inherit from Foo,
# the resulting type is Foo+
foo = rand < 0.5 ? bar : baz
typeof(foo) # => Foo+

同じサイズのタプルのユニオン

同じサイズの2つのタプルのユニオンは、各位置の型のユニオンを持つタプル型になります。

例:

t1 = {1, "hi"}   # Tuple(Int32, String)
t2 = {true, nil} # Tuple(Bool, Nil)

t3 = rand < 0.5 ? t1 : t2
typeof(t3) # Tuple(Int32 | Bool, String | Nil)

同じキーを持つ名前付きタプルのユニオン

同じキー(順序は関係ありません)を持つ2つの名前付きタプルのユニオンは、各キーの型のユニオンを持つ名前付きタプル型になります。キーの順序は、左側のタプルからの順序になります。

例:

t1 = {x: 1, y: "hi"}   # Tuple(x: Int32, y: String)
t2 = {y: true, x: nil} # Tuple(y: Bool, x: Nil)

t3 = rand < 0.5 ? t1 : t2
typeof(t3) # NamedTuple(x: Int32 | Nil, y: String | Bool)