プロトコルで既定の付属型を使う

Swift プログラミング

Swift のプロトコルでは既定の付属型を指定できます。

これはプロトコル拡張を組み合わせたときに威力を発揮するので、今回はそれについて説明します。


Swift のプロトコルでは、付属型に既定の型を指定できるようになっています。それとプロトコル拡張とを組み合わせると、プロトコルの表現力がぐっと高まるので、今回はそれについて紹介します。

既定の付属型

付属型には既定の型を指定できます。指定方法は簡単で、付属型を定義するときに = を使って既定の型を記載します。

protocol MyProtocol {
    
    associatedtype Some = Int
    
    func method(value: Some) -> Any
}

例えばこのようにしてあげると、付属型 Some は、既定では Int が想定されていることが記載できます。

ここまでだと、適用は今まで通り

このようにプロトコルを定義した時、これを例えば MyStructA という型に適用する時は、プロトコルで規定されている必須の実装を定義することになります。これについては既定の型使わないときと同じです。次のようにすることで、付属型 Some として Int が適用されて、プロトコルが求める要求を満たせます。

struct MyStructA : MyProtocol {

    func method(value: Int) -> Any {

        return "A : \(value)"
    }
}

let a = MyStructA()

a.method(1)       // "A : 1"

そしてこのとき大切になるのが、この付属型はあくまでも既定の型なので、実装する側で自由に入れ替えられるところです。

struct MyStructB : MyProtocol {

    func method(value: String) -> Any {

        return "B : \(value)"
    }
}

このようにすることで、付属型を String で持つ MyStructB を作成することも可能です。

let b = MyStructB()

b.method("s")       // "B : s"

ただ、これだけだと既定の型を利用する価値がよくわかりません。

いくら既定の型を指定しても、自分で実装するならわざわざ この付属型は普通は Int だよ みたいな意思表示って必要ないし、そもそも別の型に普通に置き換えられるので、これなら別に既定の型なんて指定しなくても普通に成り立ちます。

プロトコル拡張と組み合わせる

既定の付属型が威力を発揮するのはプロトコル拡張と合わせて使った場合です。プロトコル拡張を使うと、プロトコルに既定の振る舞いを添えることができますけど、このときに既定の型で縛った拡張を作ります。

protocol MyProtocol {
    
    associatedtype Some = Int
    
    func method(value: Some) -> Any
}

extension MyProtocol where Some == Int {

    func method(value: Int) -> Any {
    
        return "P : \(value)"
    }
}

このようにすることで、いわば 付属型の既定の型は Int で、その時の既定の実装はこれ みたいな表現ができるようになります。つまり、こうすることによって、プロトコル適用時にそもそもの付属型と実際の型とを関連付けるところから省略した実装が書けるようになります。

struct MyStructA : MyProtocol {

}

つまり、今回の例では上記のコードだけで、構造体 MyStructAMyProtocol が示す性質を備えることができます。

let a = MyStructA()

a.method(1)       // "P : 1"

もちろん全然別の型を指定して、自分で実装することもできます。このとき既定の付属型で縛った振る舞いは実装されないので、プロトコルの既定の型や実装に左右されることなく、適切な振る舞いを型に乗せることが可能です。

struct MyStructB : MyProtocol {

    func method(value: String) -> Any {

        return return "B : \(value)"
    }
}

プロトコルにおける付属型の既定の型は、このような感じで使えるようになっています。

ただ、これだけだと単純に 実装を省略できる便利機能 という印象で、それ以上のことがよくわかりません。単に実装が楽になるだけでは意味がないので、そもそもの存在意義についても考えてみることにします。

実際に使用されている例

そんな、既定の付属型を指定することの存在意義を気にして見たとき、Swift 標準ライブラリの CollectionType にとても興味深い様子が見られます。

protocol CollectionType : Indexable, SequenceType {

    associatedtype Generator : GeneratorType = IndexingGenerator<Self>
    
    func generate() -> Self.Generator
}

これは CollectionType の定義の中から、今回の話と関係するところだけを抜粋したものですけれど、ここで Generator に対して既定の型 IndexingGenerator が指定されているのが見て取れます。

IndexingGenerator

この IndexingGenerator というのは 要素を次々と生成していける GeneratorType の性質 に則った型ですけれど、具体的な特徴としては Indexable なものから順番に要素を取得できるジェネレーター ということになります。

struct IndexingGenerator<Elements : Indexable> : GeneratorType, SequenceType {

    init(_ elements: Elements)
    
    mutating func next() -> Elements._Element?
}

ちなみに Indexable というのは 最初から終端までのインデックスを規定して、その範囲内をインデックスで自由にアクセスできる性質 を持つので、つまり IndexingGenerator というのは、この 最初から終端までを順番に取得することで、ジェネレータの役割を担ってしまおう という意図を持った型になっていると思われます。

CollectionType は SequenceType の性質も持つ

ところでこのとき CollectionTypeSequenceType の要件を満たすことが求められています。

protocol CollectionType : Indexable, SequenceType {

}

ここで SequenceType というのは、肝心な定義は次のような定義になっていて、要は 内包する要素を順番に取り出していける 性質を表現したものになります。

protocol SequenceType {

    associatedtype Generator : GeneratorType

    func generate() -> Self.Generator
}

これを CollectionType が内包しているという意味は、つまり 内包してしている要素を、インデックスの先頭から末尾まで順番に取得していける という要件を満たすことが求められている、という感じに捉えて良さそうです。

CollectionType は Indexable の性質も持つ

ここで思い出したいことなのですけど、CollectionTypeIndexable の性質を持っています。

protocol CollectionType : Indexable, SequenceType {

}

つまり CollectionTypeIndexingGenerator の意図を体現する力を持っているとも言えます。

そして先ほどの SequenceType の要件を満たすこと、つまり Generator の提供が求められている のを思い出したとき、それは CollectionType既にそれを発揮する力を潜在している と捉えることができると思います。

既定の付属型とプロトコル拡張で潜在能力を発揮

整理すると CollectionTypeSequenceType 性によって Generator の実現を求められている。しかし Indexable によって既にその能力は IndexingGenerator という実現手段が潜在している。

それだったら、もはやプログラマーに判断を委ねる必要はなくて、そもそもの CollectionType の性質として、それらをまるっと表現してしまえば良くて。

protocol MyCollection : Indexable, SequenceType {
    
    associatedtype Generator : GeneratorType = IndexingGenerator<Self>
    
    func generate() -> Generator
}

extension MyCollection where Generator == IndexingGenerator<Self> {
    
    func generate() -> Generator {
        
        return IndexingGenerator(self)
    }
}

このように表現することで、このプロトコルに準拠させる型は Indexable 性さえ規定してあげれば、必然的に SequenceType の能力を発揮してくれます。実際、標準ライブラリの CollectionType が、この方法で SequenceType 性を発揮しています。

これを実際に使ってみると、次のような感じになります。

struct MyValues : MyCollection {
    
    var startIndex: Int {
    
        return 1
    }
    
    var endIndex: Int {

        return 10
    }
    
    subscript (index: Int) -> String {
        
        return String(index)
    }
}

これだけの実装で、すぐに Generator を使った振る舞いができるようになります。

例えば基本構文の for ... inGenerator で取得できる要素を順番に処理するものなので、次のようにして繰り返し処理が可能になります。ここで使われる GeneratorIndexingGenerator<MyValues> です。

let values = MyValues()

for value in values {

}

ちなみに MyCollection に宣言した func generate() -> Generator が、少なくとも Swift 2.2.1 においては、けっこう大事になってくるようです。今回の場合は SequenceType に同等のものが定義されていて、それに対してここで既定の型を追加していますけど、もしかするとどうやらここでメソッドを宣言しないと、MyCollection で定義した generateSequenceType で定義された generate とが、特に付属型を型推論で決定した時に、同一視されずにオーバーロードされてしまう可能性が出てきてしまうみたいです。そのためこうして、肝心の付属型に関連する概念(機能)を再び規定するようにしてみました。

独自の SequenceType 性を表現可能

こうして用意したプロトコルであっても、実際に使うときになって いや、この型に限っては違うジェネレーターを返したいんだ みたいなとこもあるかもしれません。そんなときにも、先ほどプロトコルで定義した既定の性質に邪魔されることはありません。

例えば MyCollection の性質を持っていながら、でも 内包する要素はランダムに取り出したい みたいな要望があるかもしれません。そんなときには既定の型を無視して、次のように実装できます。

struct MyRandomSequencialValues : MyCollection {
    
    var startIndex: Int {

        return 1
    }
    
    var endIndex: Int {

        return 10
    }
    
    subscript (index: Int) -> String {
        
        return String(index)
    }
    
    func generate() -> AnyGenerator<String> {
        
        let indices = Array(startIndex ..< endIndex)
        let randomicSortedValues = indices
            .map { (index: $0, order: arc4random()) }
            .sort { $0.order < $1.order }
            .map { self[$0.index] }
        
        return AnyGenerator(randomicSortedValues.generate())
    }
}

このようにすることで、このインスタンスを例えば for ... in で繰り返し処理を行ったときには、内包する要素をそのときどきのランダムな並びで取得できるコレクション (MyCollection) として振舞ってくれます。

let values = MyRandomSequencialValues()

for value in values {

	// 内包する要素が、そのときどきでランダムな並び順で value に取得されます。
	
}

既定の振る舞いは実装されない

このように既定の付属型と振る舞いがプロトコルに規定されているときでも、型にはそれとは別に独自実装した場合には、既定のものは実装されません。

// まずは、既定の付属型に従った型の場合
let values = MyValues()

// プロトコルの規定により、型で実装しなくても IndexingGenerator が備わっている。
var gen0_1 = values.generate() as IndexingGenerator  // OK

// AnyGenerator についてはどこでも語られていないので存在しない。
var gen0_2 = values.generate() as AnyGenerator  // NG

// そして、独自に Generator を既定した型の場合
let values = MyRandomSequencialValues()

// 独自実装を行ったことで IndexingGenerator は存在しなくなる。
var gen0_1 = values.generate() as IndexingGenerator  // NG

// 独自に実装した AnyGenerator が Generator として使える。
var gen0_2 = values.generate() as AnyGenerator  // OK

それは、プロトコル拡張のところで、既定の付属型だけに特化した振る舞いとして定義したので当然とも言えますけれど、ともあれこうして、プロトコルが表現する既定の性質に従う限りは何もしなくても実装されて、従わないときには余計な機能を持ち込まないところが、とても嬉しいように思います。

プロトコルの性質という観点、そして型の実装という観点、どちらの視点から見ても見事で、とても美しい世界に感じられました。