コンテンツにスキップ

ユニオン型

変数や式の型は複数の式から構成されることがあります。これをユニオン型と呼びます。例えば、異なるifの分岐節で同じ変数に代入したときを考えます。

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

a # : Int32 | String

if 式の終わりでは、aInt32 | String という型になります。この型は「Int32 と String のユニオン型」を表します。ユニオン型はコンパイラによって自動的に作られます。実行時には a はただ1つの型の値になります。この型は class メソッドを呼び出すことで確認できます。

# 実行時の型
a.class # => Int32

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

# コンパイル時の型
typeof(a) # => Int32 | String

ユニオン型はいくらでもたくさんの型から構成することができます。ユニオン型の式に対してメソッドを呼び出すとき、ユニオン型の中のすべての型でそのメソッドが飛び出せる必要があります。そうでなければコンパイル時にエラーとなります。また、その呼び出しの型は、すべての型での呼び出し結果の型のユニオン型になります。

# to_s は Int32 と String で定義されていて、どちらも String を返す
a.to_s # => String

a + 1 # String#+(Int32) は存在しないのでエラーになる

コンパイル時に、変数をユニオン型として定義するには次のようにします。

# コンパイル時の型を設定する
a = 0.as(Int32 | Nil | String)
typeof(a) # => Int32 | Nil | String

ユニオン型に関する規則

一般的に、T1T2 からユニオン型を構成するとき、その結果のユニオン型は T1 | T2 となります。ですが、いくつかの場合にそれとは異なる型になることがあります。

継承元の同じクラス/構造体のユニオン型

T1T2 が継承元を共有していて、共通する祖先の型 ParentReferenceStructIntFloat もしくは Value ではないとき、2つからユニオン型を構成しようとしたときの結果の型は Parent+ となります。この型はバーチャル型と呼ばれるものです。コンパイラはこれを、Parent もしくはそのサブクラスを表すものと見なします。

例をあげます。

class Foo
end

class Bar < Foo
end

class Baz < Foo
end

bar = Bar.new
baz = Baz.new

# ここで foo の型は Bar | Baz となりそうだが、
# Bar も Baz も Foo を継承しているので、
# 結果的に型は Foo+ となる。
foo = rand < 0.5 ? bar : baz
typeof(foo) # => Foo+

同じ大きさのタプルのユニオン型

同じ大きさのタプル同士のユニオン型は、タプルの各型のユニオン型を取ったものになります。

例をあげます。

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)

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

キーの同じ(順序は考慮しない)名前付きタプル同士のユニオン型は、名前付きタプルの対応するキーの型同士のユニオン型となります。キーの順序は左辺の型の順序のものとなります。

例をあげます。

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)