SwiftのArrayがミュータブルでもCovariantな理由

More than 3 years have passed since last update.

SwiftのArrayでは次のようなことができます。


(A)

class Animal {}

class Cat: Animal {}

var cats: Array<Cat> = [Cat(), Cat()]
var animals: Array<Animal> = cats

// 実体はCatのArrayのはずなのにAnimalを格納できる!?
animals[0] = Animal()


これを見ると型安全性がぶっ壊れてるんじゃないかと思ってしまいますが、そうではありません。SwiftのArrayにとってこれは安全な挙動です。


Covariantなコレクション

(A) のように、 CatAnimal の派生型なら Array<Cat>Array<Animal> の派生型となるとき、 ArrayCovariant(共変) であると言います[*1]

Array<Cat>Animal オブジェクトが格納できてしまうように、ミュータブルなコレクションがCovariantだとそのコレクションに本来格納できない型の要素が格納できることになり、型安全ではありません[*2]。そのため、ミュータブルなコレクションはCovariantとして宣言できません。(普通は Invariant(不変) として宣言されます。)

一方で、 イミュータブルなコレクションはCovariantであっても問題ありません 。要素が変更されないのであれば、 Array<Cat>Array<Animal> として安全に振る舞うことができるからです。


(B)

var cats: Array<Cat> = [Cat(), Cat()]

var animals: Array<Animal> = cats

// 要素が変更されないなら何も問題なし
for animal: Animal in animals {
println(animal)
}


実際に、C#やScala、Ceylonなどの言語では、イミュータブルなコレクションがCovariantとして宣言されています。


SwiftのArrayがミュータブルでもCovariantなのはなぜか

SwiftのArrayはクラスではなく構造体です。そして、構造体は値型です。

論理的には、 値型はイミュータブルなクラスと等価 と考えられます。次のコードの Int は、値型と考えてもイミュータブルなクラスと考えても問題ありません[*3]


(C)

var a: Int = 123

a = a + 1

Int が値型だと考えてみましょう。 a + 1 を実行しても 123 という数そのものが変更されたわけではなく、 1231 を足した 124 という新たな値が a に代入されるだけです。 123123 であり、 123124 になったわけではありません。

これは、 Int がイミュータブルなクラスだとして次のように考えた場合と等価です。 123 というオブジェクトに 1 を足した結果の新たな 124 というオブジェクトを生成し、それを a に代入した、と。

よって、


  • イミュータブルなコレクションはCovariantでも型安全

  • 値型はイミュータブルなクラスと等価

  • SwiftのArrayは値型

から、 SwiftのArrayはCovariantでも型安全 と言えます。


値型は本当にイミュータブルなクラスと等価か

ちょっと待って下さい!次の例を見て下さい。


(D)

struct Fraction { // 分数

var numerator: Int // 分子
var denominator: Int // 分母
}

var fraction = Fraction(numerator: 1, denominator: 3)
fraction.numerator = 2 // 値そのものを変更できた!


値型はイミュータブルなクラスと等価だと書きましたが、fractionに代入された値そのものが変更されています。これはどう考えれば良いでしょうか?

Fractionをイミュータブルなクラスとしたときに、対応するコードは次のようになります。


(E)

class Fraction { // 分数

let numerator: Int // 分子
let denominator: Int // 分母
init(numerator: Int, denominator: Int) {
self.numerator = numerator
self.denominator = denominator
}
}

var fraction = Fraction(numerator: 1, denominator: 3)
fraction = Fraction(numerator: 2, denominator: fraction.denominator)


このように、構造体のメンバを書き換える操作は、イミュータブルなオブジェクトを対応するメンバだけ変えて再生成することに対応します。

この二つが等価なことは、次の二つを比較するとよくわかります。


(D'

var a = Fraction(numerator: 1, denominator: 3)

var b = a
a.numerator = 2 // a == 2/3, b == 1/3


(E'

var a = Fraction(numerator: 1, denominator: 3)

var b = a
a = Fraction(numerator: 2, denominator: a.denominator) // a == 2/3, b == 1/3

つまり、


(A)の抜粋

animals[0] = Animal()


のコードは意味的には次のようなものなのです。


(F)

// animalsをコピーして0番目の要素をAnimal()に変えたArrayを再生成

animals = replace(animals, 0, Animal())

Array を変更しているように見えても実際には新しい値を生成しているのと同じだから、 Array はイミュータブルな型だと考えることができ、Covariantでも安全なのです。


もっと簡単な考え方

Array は値型なので


(A)の抜粋

var animals: Array<Animal> = cats


の時点で Array 全体がコピーされます。 animalscats から要素をコピーされた新しい Array<Animal> なので、その後 Animal オブジェクトが格納されようと cats には関係ありません。


Arrayを代入したり要素を変更する度にコピーしたらパフォーマンスが悪いのでは?

実際には、要素を変更する度に Array がコピーされるわけではありません。コピーしないと安全性が保てない場合にだけコピーされます。

"SwiftのArrayが実はすばらしかった"で、コピーが頻発しているように見えてもパフォーマンスが悪化しない理由を説明しています。(要素の変更については書いていませんが同じように考えられます。)


まとめ

次の理由から、 SwiftのArrayはミュータブルでCovariantでも型安全 です。


  • イミュータブルなコレクションはCovariantでも型安全

  • 値型はイミュータブルなクラスと等価

  • SwiftのArrayは値型


[*1] 逆に、 BA の派生型のときに Foo<A>Foo<B> の派生型として扱えるとき、 FooContravariant(反変) であると言います。

[*2] 実際にはコレクションに限らずジェネリックな型で一般的に起こることだし、要素を追加しなくても Array#search(Element) のようなメソッドもアウトですが、ややこしくなるだけなので話を限定して進めます。

[*3] 実際、Scalaの Int はイミュータブルなクラスですが、処理系では値型(プリミティブ型)であるJavaの int と等価なものとして扱われ、高速に計算されます。