Edited at
SwiftDay 23

Conditional Conformanceで遊ぼう

Conditional ConformanceはSwift4.1で追加された言語機能です。

型パラメータに条件をつけて(Conditional)他のProtocolに適合する(Conformance)ことができる便利な機能です。

class Box<T> {

var value: T
init(_ value: T) { self.value = value }
}

解説用の箱です。これをConditional Conformanceで拡張して遊んでみましょう。最近私や身の回りの人が踏んだものを一通り紹介します。


前提となるProtocolは明示的に宣言する必要がある

// Conditional conformance of type 'Box<T>' to protocol 'Hashable' does not imply conformance to inherited protocol 'Equatable'

extension Box: Hashable where T: Hashable {
func hash(into hasher: inout Hasher) {
value.hash(into: &hasher)
}
}

HashableはEquatableを前提に持つProtocolです。ConditionalConformanceでHashableを使う場合は、「明示的にEquatableのConditionalConformanceを宣言する」必要があります。


複数のConditionから同一のConformanceは作れない

// Conflicting conformance of 'Box<T>' to protocol 'Hashable'; there cannot be more than one conformance, even with different conditional bounds

extension Box: Hashable where T: AnyObject {
func hash(into hasher: inout Hasher) {
return ObjectIdentifier(self).hash(into: &hasher)
}
}

// Conflicting conformance of 'Box<T>' to protocol 'Hashable'; there cannot be more than one conformance, even with different conditional bounds
extension Box: Hashable where T == Any.Type {
func hash(into hasher: inout Hasher) {
return ObjectIdentifier(self).hash(into: &hasher)
}
}

一度HashableにConformしたBoxは他の方法でHashableにConformできなくなります。

滅多にこの要求は発生しないものですが、ObjectIdentifierを使って良しなに動かす夢は潰えました。


Existentialが作れる

extension Box: CustomStringConvertible where T: CustomStringConvertible {

var description: String { return value.description }
}

func check(_ value: Any) {
guard let value = value as? CustomStringConvertible else { return }
print(value)
}

check(Box(1))
check(Box(Box(1)))

Swift4.2からExistentialが使えるようになりました。Swift4.1ではキャストの実行時に警告(warning: Swift runtime does not yet support dynamically querying conditional conformance)が出ていたものです。


Existentialを作るとクラッシュする

作れると言ったな?あれは嘘だ


extension Box: CustomStringConvertible where T: Sequence, T.Element :CustomStringConvertible {
var description: String {
return "[" + value.map { $0.description }.joined(separator: ", ") + "]"
}
}

check(Box([1, 2, 3])) // EXC_BAD_ACCESS (code=EXC_I386_GPFLT

Conditionの複雑さが以下の条件を超えると実行時クラッシュです。


  • associatedtypeを持つprotocolに条件付をする

  • associatedtypeにも条件付をする

Bugsに報告されています https://bugs.swift.org/browse/SR-8666


typealiasはconditionalではなく、全体を汚染する。

数日前にDiscordで盛り上がっていたネタです。

extension Box: Sequence where T: Sequence {

typealias Element = T.Element
typealias Iterator = T.Iterator

func makeIterator() -> T.Iterator {
return value.makeIterator()
}
}

例えばSequenceのConditional Conformanceを書いてみましょう。

一見すると問題なさそうですが、ElementはT: Sequenceでない場合にも存在してしまいます。いろいろ困ったことが発生します。

// error: Segmentation fault: 11

print(Box<Int>.Element.self)

Box.Elementは存在しているのですが、Int.Elementは存在しない、これはコンパイル時にセグフォが発生します。

extension Box: IteratorProtocol where T: IteratorProtocol {

// Invalid redeclaration of 'Element'
typealias Element = T.Element

func next() -> Element? {
return value.next()
}
}

加えてIteratorProtocolのConditional Conformanceを追加しました。Elementは汚染されているので宣言することが出来ません。ところでこの場合は汚染されたElementはT.Elementなので、typealiasを消してそのまま利用すれば、「たまたま正しく」動きます。

極めて稀に発生する、ElementをT.Element以外で指定したいという要求が発生すると詰みます。他の方法を探しましょう。

Bugsに報告されています https://bugs.swift.org/browse/SR-9533


おわり

Conditional Conformanceで遊んでみました。残念ながら少し壊れてしまいました。

とはいえConditional Conformanceの用途の80%は「ElementがPならWrapperもPにしたい」というもので今回紹介した悪いコードを使う必要は殆どありません。でも20%ぐらいは壊しそうになること、ありますよね。

20%の具体例ですが @taketo1024 先生が書いているSwiftyMathが、限界を超えそうなConditional Conformanceを要所要所で利用されていて面白いです。

https://github.com/taketo1024/SwiftyMath