まとめ: 型システムが微妙
-
[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 はまだ進化を続けている言語ですから、ここで挙げている問題は将来的に解決されるかもしれません。