86
84

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Swiftの型の深淵をのぞく

Posted at

深淵をのぞくとき、深淵もまたこちらをのぞいているのだ
by フリードリヒ・ニーチェ

一緒に Swift の型の深淵をのぞいてみましょう。

継承と型の派生

※ 以下、 クラス継承is-a 関係 について知っていることを前提とします。

Animal クラスと、それを継承した Cat クラスがあるとします。

class Animal {}
class Cat: Animal {}

このとき、 CatAnimalサブタイプ( Subtype ) となるので、 Animal 型の変数に Cat のインスタンスを代入することができます(逆に、 AnimalCatスーパータイプ( Supertype ) であると言います)。

let animal: Animal = Cat()

ここまでは何の変哲もない話です。

関数と型の派生

※ 以下、 クロージャ式 と 関数の型 ( () -> FooBar -> () のような表記)について知っていることを前提とします。

さて、↑ではあえてサブクラスと言わずにサブタイプと言いました。なぜでしょう?型の派生はクラスでだけ起こるものではないからです。

戻り値の型と共変性

関数にも派生関係があります。たとえば、 () -> Cat() -> Animal のサブタイプになります。

let getCat: () -> Cat = { Cat() }
let getAnimal: () -> Animal  = getCat

CatAnimal のサブタイプであるため、 Cat を返す関数は常に Animal を返します。そのため、 Cat を返す関数は Animal を返す関数として振る舞えるのです( () -> Cat() -> Animal のサブタイプ)。

CatAnimal のサブタイプであれば () -> Cat() -> Animal のサブタイプとなることを戻り値の型について 共変( Covariant) であると言います。

引数の型と反変性

次に、引数について考えてみましょう。 Cat -> ()Animal -> () のサブタイプでしょうか?

let useCat: Cat -> () = { _ in }
let useAnimal: Animal -> () = useCat // コンパイルエラー

コンパイルエラーになってしまいました。なぜでしょう?もし、 useCatuseAnimal に代入できると、 useAnimal に代入された useCatAnimal が渡されてしまうかもしれません。 useCatCat しか受け取れないので、 Animal が渡せてしまうのはおかしいということになり、型エラーとなるわけです。つまり、 Cat -> ()Animal -> () としては振る舞えない(サブタイプではない)のです。

しかし、次のコードは OK です。

let useAnimal: Animal -> () = { _ in }
let useCat: Cat -> () = useAnimal

何が起こったのでしょう? CatAnimal のサブタイプなのに、 useAnimaluseCat に代入できるのは一見変な気がします。しかし、 useCatCat しか受け取らないので、 useCat に代入された useAnimalCat しか受け取ることはありません。 CatAnimal のサブタイプなので useAnimalCat を受け取っても問題ないわけです。つまり、 Animal を受け取る関数は Cat を受け取る関数として振る舞えるのです( Animal -> ()Cat -> () のサブタイプ、 Cat -> ()Animal -> () のスーパータイプ)。

CatAnimal のサブタイプのとき、関係が反転して Cat -> ()Animal -> () のスーパータイプとなることを引数の型について 反変( Contravariant ) であると言います。

戻り値と引数をまとめる

戻り値の話と引数の話をまとめて考えると、次のような派生関係も考えることができます。

  • Cat -> CatCat -> Animal のサブタイプ
  • Animal -> AnimalCat -> Animal のサブタイプ
  • Animal -> CatCat -> Animal のサブタイプ

ジェネリクスと型の派生

※ 以下、 ジェネリクス について知っていることを前提とします。

Swift では部分的にしかサポートされていませんが、プログラミング一般にはジェネリクスも型の派生を作ります。

ジェネリクスと共変性

ただ、値を入れて取り出せるだけの次のようなクラスを考えてみましょう1

class Box<T> {
    private let value: T
    init(value: T) { self.value = value }

    func get() -> T {
        return value
    }
}

valueprivate にしなければ get なんかいらないんですが、プロパティよりもメソッドで話をした方がわかりやすいので、あえて get メソッドを作って話を進めます。

このとき、 Box<Animal>Box<Cat> の間に派生関係は生まれるでしょうか?

let catBox: Box<Cat> = Box(Cat())
let animalBox: Box<Animal> = catBox
let animal: Animal = animalBox.get()

catBoxgetCat を、 animalBoxgetAnimal を返します。 animalBox に代入された Box<Cat>getCat を返しますが、 CatAnimal のサブタイプなので Box<Animal>get として振る舞う上で問題はありません。

Box には get 以外の公開されたメンバはないので、他に検討しなければならないものはありません。 Box<Cat>Box<Animal> として振る舞うことができそうです。

CatAnimal のサブタイプであれば Box<Cat>Box<Animal> のサブタイプになることを、型パラメータ T について共変であると言います。関数の戻り値と似た話ですね。

一般的に、ジェネリックな型 Foo<T> があって、型パラメータ T がメソッドの戻り値にしか使われてない場合(引数に使われていない場合)、FooT について共変になります2

残念ながら、 Swift 2.1 の時点では自作の型でこれを実現することができません。しかし、 ArrayOptional などの一部の標準ライブラリの型は、型パラメータについて共変になっています3

let catArray: Array<Cat> = [Cat(), Cat(), Cat()]
let animalArray: Array<Animal> = catArray

つまり、 Array<Cat>Array<Animal> のサブタイプなわけです。同様に、 Optional<Cat>Optional<Animal> のサブタイプです。

余談ですが、 C#, Scala, OCaml , Kotlin, Ceylon などの言語では型パラメータについての変性(共変、反変など)がサポートされています。その場合でも、型パラメータが戻り値でしか使われていないからといって自動的に共変になるわけではありません。次のように、型パラメータに明示的に修飾子(下記の例では out )を付与し、もしその型パラメータが引数で使われたらコンパイルエラーとなります。

// Kotlin
class Foo<out T> { // T に out をつけると T について共変になる
    fun foo(): T { ... } // 戻り値でのみ T が使える
}

ジェネリクスと反変性

同じように、ジェネリックな型 Foo<T> があって、型パラメータ T がメソッドの引数にしか使われてない場合(戻り値に使われていない場合)、FooT について反変になります。つまり、そのような Foo について、 Foo<Cat>Foo<Animal> のスーパータイプになります。

共変の場合同様、残念ながら、 Swift 2.1 の時点では自作の型でこれを実現することができません。また、僕の知る限り、型パラメータについて反変になっている型は Swift に存在しません。

やはり共変のときと同じように、型パラメータについての変性がサポートされている言語でも、型パラメータが引数でしか使われていないからといって自動的に反変になるわけではありません。

// Kotlin
class Foo<in T> { // T に in をつけると T について反変になる
    fun foo(x: T) { ... } // 引数でのみ T が使える
}

Optionalと型の派生

※ 以下、 Optional type について知っていることを前提とします。

ここからようやく深淵に少し足を踏み入れるんですが、 Optional も型の派生を作ります。

let a: Int = 42
let b: Optional<Int> = a

上記のコードをそのまま解釈すれば、 Optional<Int> 型の変数に Int インスタンスを代入できているので IntOptional<Int> のサブタイプだということになります。しかし、いくらなんでもこれは暗黙の型変換だろうと考えるのが普通です。

let a: Int = 42
let b: Optional<Int> = Optional(a) // 暗黙の型変換されていた箇所を明示的に書いた

しかし、実は Swift では IntOptional<Int> のサブタイプです。それを示します。

戻り値についての共変性は、クラスの継承においても適用されます。つまり、メソッドをオーバーライドして、戻り値の型をサブタイプに変更しても構いません。

class A {
    func animal() -> Animal {
        return Animal()
    }
}

class B: A {
    // override して戻り値の型を Animal のサブタイプである Cat に変更
    override func animal() -> Cat {
        return Cat()
    }
}

上記のコードの AnimalCat と同じことが Optional<Int>Int でもできます。

class A {
    func foo() -> Optional<Int> {
        return nil
    }
}

class B: A {
    // override して戻り値の型を Optional<Int> のサブタイプである Int に変更
    override func foo() -> Int {
        return 42
    }
}

これができるとなると、暗黙の型変換というよりサブタイプであると解釈した方が自然です。

Optional は次のように宣言されていますが、同じような型を自作してもこんなことは実現できません。 Optional だけが特別なのです。

enum Optional<Wrapped> {
    case None
    case Some(Wrapped)
}

// 同じような型を自作しても Int は MyOptional<Int> のサブタイプにはならない
enum MyOptional<Wrapped> {
    case None
    case Some(Wrapped)
}

余談ですが、関連のある興味深い話として Union type があります。 Ceylon では Integer|String のように書くと IntegerString のどちらかを入れることのできる型を作ることができます。

// Ceylon
Integer a = 42;
Integer|String b = a;

この例でわかるように、 IntegerInteger|String のサブタイプです。そして興味深いのは、 Swift では Int? と書けば Optional<Int> のことですが、 Caylon では Integer? と書けば Integer|Null という Union type になることです。そのため、 Ceylon の IntegerInteger? のサブタイプになります。このサブタイピングは Swift の IntInt? の関係と同じです。

Union type は、 Ceylon の他に、 TypeScript, Flow, Python の型ヒントなどで導入されています。特に、 Python の Optional[Foo]Union[Foo, None] であるなど Ceylon そっくりです。

型の型と型の派生

さて、前置きが長くなりましたがここからが本題です。

Swift では次のようにして型の型(メタタイプ)を得ることができます。

let intType: Int.Type = Int.self // Int の型を取得
let zero: Int = intType.init() // let zero: Int = Int() とするのと同じ

また、次のようにして、インスタンスからメタタイプを取得することもできます。

let n: Int = 42
let intType: Int.Type = n.dynamicType

継承の場合

さて、ここで AnimalCat のメタタイプについて調べると面白いことがわかります。

let catType: Cat.Type = Cat.self
let animalType: Animal.Type = catType

なんと、 Cat.Type インスタンスを Animal.Type インスタンスに代入できました。これは、 Cat.TypeAnimal.Type のサブタイプだということを意味しています。メタタイプは、元の型について共変なわけです。

では、これまでに見てきた継承以外のサブタイプのメタタイプを調べるとどうなるでしょうか。

関数の場合

関数のメタタイプは (() -> Cat).self などと書くとコンパイルエラーになってしまったので、 dynamicType を使って取得します。

let getCat: () -> Cat = { Cat() }
let voidToCatType: (() -> Cat).Type = getCat.dynamicType

さて、継承と同じであれば voidToCatType(() -> Animal).Type 型の変数に代入できるはずです。

let voidToAnimalType: (() -> Animal).Type = voidToCatType // コンパイルエラー
error: cannot convert value of type '(() -> Cat).Type' to specified type '(() -> Animal).Type'

なんと!エラーになってしまいました。

Animal -> ()Cat -> () についても同様です。

let useAnimal: Animal -> () = { _ in }
let animalToVoidType: (Animal -> ()).Type = useAnimal.dynamicType
let catToVoidType: (Cat -> ()).Type = animalToVoidType // コンパイルエラー
error: cannot convert value of type '(Animal -> ()).Type' to specified type '(Cat -> ()).Type'

関数のメタタイプについては共変にならないようです。

ジェネリクスの場合

次は、 Array<Animal>Array<Cat> について調べましょう。

let catArrayType: Array<Cat>.Type = Array<Cat>.self
let animalArrayType: Array<Animal>.Type = catArrayType // コンパイルエラー
error: cannot convert value of type 'Array<Cat>.Type' to specified type 'Array<Animal>.Type'

ジェネリクスのメタタイプもダメでした。

Optionalの場合

次は Optional<Int>Int です。

let intType: Int.Type = Int.self
let optionalIntType: Optional<Int>.Type = intType // コンパイルエラー
error: cannot convert value of type 'Int.Type' to specified type 'Optional<Int>.Type'

Optional のメタタイプもダメでした。

メタタイプの場合

次は Animal.TypeCat.Type です。

let catTypeType: Cat.Type.Type = Cat.Type.self
let animalTypeType: Animal.Type.Type = catTypeType // コンパイルエラー
error: cannot convert value of type 'Cat.Type.Type' to specified type 'Animal.Type.Type'

メタメタタイプもダメでした。

補足

※ 以下、 値型参照型 について知っていることを前提とします。

  1. 継承
  2. 関数
  3. ジェネリクス
  4. Optional
  5. メタタイプ

これらのメタタイプの、元の型についての 変性(Variance) を調べましたが、 1 のみ共変となっており、それ以外は共変でない( 非変( Invariant ) な)という結果になりました4

実は上記の五つの中でサブタイプを作るか怪しいものがあります。それはジェネリクスです。

let catArray: Array<Cat> = [Cat(), Cat(), Cat()]
let animalArray: Array<Animal> = catArray

上記のコードを見ると、一見 Array<Cat>Array<Animal> のサブタイプであるように見えます。しかし、僕はこれは暗黙の型変換なのではないかと疑っています( IntOptional<Int> に代入するときに暗黙の型変換のように思えるけど実はサブタイプになっていることと反対なのがおもしろいですね)。なぜなら、 Array は値型であるため、 Array<Cat>Array<Animal> のように振る舞うなどという話以前に、代入時点でコピーされるからです5

つまり、実際には次のようになっているのではないかと考えられるわけです。

let catArray: Array<Cat> = [Cat(), Cat(), Cat()]
let animalArray: Array<Animal> = Array(catArray)

Optional のとき同様に、オーバーライドを使って検証してみましょう。

class A {
    func animals() -> Array<Animal> {
        return []
    }
}

class B: A {
    // override して戻り値の型を Array<Animal> のサブタイプである Array<Cat> に変更
    override func animals() -> Array<Cat> { // コンパイルエラー
        return []
    }
}
error: method does not override any method from its superclass

ダメでした。ますます怪しいです。ひとまず、 Array は型パラメータについて共変になっていてサブタイプを作るわけではなく、 Array<Cat>Array<Animal> に代入できるのは暗黙の型変換と考えた方が良さそうです。

まとめ

Swift のサブタイピングをまとめ、それらのメタタイプについての派生関係を調べました。

なぜか、継承関係にあるクラスのメタタイプ同士は共変になりましたが、その他のはサブタイプのメタタイプでは非変となってしまいました。

どうしてこんなことになってしまったのでしょうか。もしかすると、 Swift の型システムの深淵をのぞきすぎてしまったのかもしれません。

言い訳

この投稿は Advent Calendar 用に 11 月に書き溜めてあったものです。その後、 Swift がオープンソース化され、型システムについて本気で調べるためにはコンパイラのソースを読まないといけなくなってしまいました。

現時点まではちゃんと読む時間を確保できておらず、また、年が明けたらしばらくは try! Swift の準備にかかりきりになりそうなので、このままお蔵入りしてしまいそうだったんですが、 Swift その2 のカレンダーの最後の 3 日が投稿されなかったということで、急遽投稿することにしました。

コンパイラの中まで調べられてないのは心残りですが、お蔵入りするよりはマシかなぁとも思います。もしかすると後日(大分あとになるかもしれませんが)、ちゃんとコンパイラを読んだ上での調査結果を書くかもしれません。


  1. Swift 1.2 の時代にはこの Box大活躍しました

  2. より厳密なルールはこちらを御覧下さい。

  3. Array には型パラメータを引数にとる append のようなメソッドがありますが、それでも共変でいられる理由についてはこちらを御覧下さい。

  4. 反変にもなっていませんでした。

  5. 実際には Copy-on-Write があるので代入時にコピーが走るわけではないですが、話の本質とは関係ありません。

86
84
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
86
84

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?