プロトコルで既定の付属型を使う
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 {
}
つまり、今回の例では上記のコードだけで、構造体 MyStructA
は MyProtocol
が示す性質を備えることができます。
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 の性質も持つ
ところでこのとき CollectionType
は SequenceType
の要件を満たすことが求められています。
protocol CollectionType : Indexable, SequenceType {
}
ここで SequenceType
というのは、肝心な定義は次のような定義になっていて、要は 内包する要素を順番に取り出していける
性質を表現したものになります。
protocol SequenceType {
associatedtype Generator : GeneratorType
func generate() -> Self.Generator
}
これを CollectionType
が内包しているという意味は、つまり 内包してしている要素を、インデックスの先頭から末尾まで順番に取得していける
という要件を満たすことが求められている、という感じに捉えて良さそうです。
CollectionType は Indexable の性質も持つ
ここで思い出したいことなのですけど、CollectionType
は Indexable
の性質を持っています。
protocol CollectionType : Indexable, SequenceType {
}
つまり CollectionType
は IndexingGenerator
の意図を体現する力を持っているとも言えます。
そして先ほどの SequenceType
の要件を満たすこと、つまり Generator の提供が求められている
のを思い出したとき、それは CollectionType
が 既にそれを発揮する力を潜在している
と捉えることができると思います。
既定の付属型とプロトコル拡張で潜在能力を発揮
整理すると CollectionType
は SequenceType
性によって 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 ... in
は Generator
で取得できる要素を順番に処理するものなので、次のようにして繰り返し処理が可能になります。ここで使われる Generator
は IndexingGenerator<MyValues>
です。
let values = MyValues()
for value in values {
}
ちなみに MyCollection
に宣言した func generate() -> Generator が、少なくとも Swift 2.2.1 においては、けっこう大事になってくるようです。今回の場合は SequenceType
に同等のものが定義されていて、それに対してここで既定の型を追加していますけど、もしかするとどうやらここでメソッドを宣言しないと、MyCollection
で定義した generate
と SequenceType
で定義された 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
それは、プロトコル拡張のところで、既定の付属型だけに特化した振る舞いとして定義したので当然とも言えますけれど、ともあれこうして、プロトコルが表現する既定の性質に従う限りは何もしなくても実装されて、従わないときには余計な機能を持ち込まないところが、とても嬉しいように思います。
プロトコルの性質という観点、そして型の実装という観点、どちらの視点から見ても見事で、とても美しい世界に感じられました。