読者です 読者をやめる 読者になる 読者になる

なるようになるかも

力は多くの場合、その人の思いを超えない。

Anyパターンについて考えてみる

Swiftの標準ライブラリだと付属型を持ったプロトコルを変数に格納するため、慣例的にAnyなんとかってクラスを用意してる」というところまでが話の前提です。

Anyクラス実装者の責務

qiita.com

例によって、Pokemonプロトコルで考えてみます。

ポケモンではすべての技(move)を使いきった場合、攻撃を選択しても「わるあがき」しかできなくなります。これはすべてのポケモン共通の挙動なので、Protocol Extensionで実装すると楽ですね。

protocol Pokemon {
    associatedtype PokemonType
    func attack(move:PokemonType)
    func struggle()
}

extension Pokemon {
    func struggle() {
        // 相手にダメージを与えて自分のHPを1/4削る
    }
}

「わるあがき」は技タイプなしの特殊攻撃なので(ゴーストタイプにも当たる)、PokemonTypeには依存しないものとして実装されるとします。

残念ながら、現世代までのポケモンで「わるあがき」で特殊な挙動をする個体はいないみたいですが、将来的に特殊な実装をされる可能性を考慮すると、適切に実装を退避させる必要があります。

class AnyPokemon <PokemonType>: Pokemon {
    private let _attack: ((PokemonType) -> Void)
    private let _struggle: (() -> Void)
    
    required init<U:Pokemon where U.PokemonType == PokemonType>(_ pokemon: U) {
        _attack = pokemon.attack
        _struggle = pokemon.struggle
    }
    
    func attack(type:PokemonType) {
        return _attack(type)
    }
    
    // 省略した場合はデフォルト実装が呼ばれてしまう!
    func struggle() {
        return _struggle()
    }
}

省略するとデフォルト実装になってしまう、というのがポイントです。

Anyタイプを作る開発者は、Protocol Extensionによるデフォルト実装も含めた、そのプロトコルの全ての処理を知る必要がある のです。

AnySequenceを実現している、_SequenceBoxの場合どうなってるのかというと、必ずしも全ての処理が委譲されるわけではないらしく、たとえばdropFirst(_ n: Int)は委譲されるのだけれど、dropFirst()(引数なし版)は委譲されません。

dropFirst()を変な処理でオーバーライドすると壊れます。やる人いないと思いますけど。

Protocol ExtensionでAnyパターンを壊す

SequenceTypeにProtocol Extensionでメソッドを足してみます。

extension SequenceType {
    func hogehoge () {
        print("do")
    }
}

let sequence = AnySequence(["a", "b"])
sequence.hogehoge() // do

AnySequenceSequenceTypeに準拠しているので、Protocol Extensionの恩恵を受けます。ここまでは期待通りです。

ただし、独自のSequenceTypeを作って、Protocol Extensionで追加したものと同名のメソッドを実装すると破綻します。

class MySequence : SequenceType {
    private var values: [String] = ["a", "b"]
    
    func generate() -> IndexingGenerator<[String]>{
        return values.generate()
    }
    
    func hogehoge() {
        print("inherit")
    }
}

let original = MySequence()
let boxed = AnySequence(original)
original.hogehoge() // inherit
boxed.hogehoge() // do
original.dropFirst().hogehoge() // do

これは_SequenceBoxの実装を考えれば自明ですね。dropFirst()でもデフォルト実装が呼び出されてしまうのは、dropFirst()Anyでラップする実装だからですね。

// 何も考えずに、Github上で master branch を見ていたので、
// Swift 2.2じゃなくて3.0のコードですが、大差ないので気にしない
@warn_unused_result
public func dropFirst() -> SubSequence { return dropFirst(1) }

@warn_unused_result
public func dropFirst(_ n: Int) -> AnySequence<Iterator.Element> {
  precondition(n >= 0, "Can't drop a negative number of elements from a sequence")
  if n == 0 { return AnySequence(self) }
  return AnySequence(_DropFirstSequence(_iterator: makeIterator(), limit: n))
}

結論

CollectionTypeなんかの組み込み型に対してProtocol Extensionする場合、Anyで型消去されたときに、Protocol Extensionのデフォルト実装しか呼び出されない点について注意が必要?

これは言語仕様的に防御した方がいいような気がする…何のメリットもないし。