まとめ: 型システムが微妙
-
[Equatable]
がEquatable
じゃない-
extension
で Protocol を実装するとき、型パラメータを特殊化できないのでこうなる
-
- Protocol が型パラメータをもてない
-
associatedtype
を持てるけど、これはメンバーや変数の型に指定できないのでワークアラウンドが必要になる
-
[Equatable]
が Equatable
じゃない
Swift では ==
で比較可能な型には Equatable
という Protocol がついています。例えば、Int
や String
、Bool
は Equatable
を実装しているので、==
によって比較可能です:
print(10 == 10) // true
print("str" == "str") // true
print(true == true) // true
この Equatable
は ==
の実装を要求するので、次のように簡単に実装できます:
struct MyEquatable: Equatable {
let value: Int
}
func ==(lhs: MyEquatable, rhs: MyEquatable) -> Bool {
return lhs.value == rhs.value
}
print(MyEquatable(value: 10) == MyEquatable(value: 10)) // true
さて、Equatable
の配列である [Equatable]
も ==
によって比較可能です:
print([10] == [10]) // true
そのため、[Equatable]
も Equatable
であると思い込みがちですが、実際はそうではありません:
// == を関数に包んでみました
func eq<E: Equatable>(_ lhs: E, _ rhs: E) -> Bool {
return lhs == rhs
}
print(eq(10, 10)) // OK, Int is an Equatable
print(eq("str", "str")) // OK, String is an Equatable
print(eq(true, true)) // OK, Bool is an Equatable
print(eq([10], [10])) // NG, argument type '[Int]' does not conform to expected type 'Equatable'
なぜか
[Equatable]
で ==
が使えるのは、==
に専用のオーバーロードが定義されているからです(該当部分):
public func == <Element : Equatable>(
lhs: Array<Element>, rhs: Array<Element>
) -> Bool {
let lhsCount = lhs.count
if lhsCount != rhs.count {
return false
}
// ...
それなのに、[Equatable]
は Equatable
を実装していないのです。
これは Swift 3.0 の機能ではできないためにこうなっています。現状の Swift では特殊化された型パラメータをもつ総称型に Protocol を実装させられないのです:
extension Array<Element>: Equatable where Element = Equatable {
func ==<Element: Equatable>(lhs: Array<Element>, rhs: Array<Element>) -> Boo
// ...
}
}
このような、特殊化された総称型のみ Protocol を実装したとみなすパターンは頻繁に必要になります。そのため、例えば Haskell であれば、比較可能であることを示す Eq
は、Eq
の配列も Eq
であるとしています:
(Eq a) => Eq [a]
Protocol が型パラメータをもてない
Swift では、Protocol が型パラメータをもてません:
protocol Foo<T> {} // error: protocols do not allow generic parameters; use associated types instead
その代わりに、associatedtype
を使うと、実質的に型パラメータを持たせられます:
protocol Foo {
associatedtype Value
}
class MyFoo<V>: Foo {
typealias Value = V
init(_ value: V) {}
}
let foo: MyFoo<Int> = MyFoo(10) // ok
ただし、この方法で Protocol を総称型にすると、次のように Protocol を変数の型に利用できなくなります:
let ng: Foo = MyFoo(10) // error: protocol 'Foo' can only be used as a generic constraint because it has Self or associated type requirements
対処法として「AnyPokemon
」というワークアラウンドがよく知られています。このワークアラウンドの詳細は割愛しますが、かなり強引な手法で、複雑で余計なコードを多く必要とする問題が依然として残されています。
終わりに
Swift が Obj-C に比べれば優れた言語であるというのはわかります。ただし、少なくとも現状の Swift を嫌う気持ちは変わりません。
なお、Swift はまだ進化を続けている言語ですから、ここで挙げている問題は将来的に解決されるかもしれません。