Quantcast
Channel: Swift Advent Calendarの記事 - Qiita
Viewing all articles
Browse latest Browse all 25

Conditional Conformanceで遊ぼう

$
0
0

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


Viewing all articles
Browse latest Browse all 25

Trending Articles