プロトコル拡張を使ってプログラミングする

カジュアル Swift プログラミング

Swift 2.0 からプロトコル拡張が導入されて、プロトコル主体のプログラミングがさらに本格化しそうです。

そんなプロトコル拡張に感じた魅力を簡単にながら整理してみました。


Swift 1.2 までは、大域関数が重要な位置を占めていました。

それはプロトコルで役割を定義して、型を問わずにプロトコル主体でコードを記述することでコードの共通化を図る、ジェネリックプログラミングが鍵になっていたためでした。

プロトコル主体の型設計

たとえば、Swift には配列型 Array がありますが、これは CollectionType プロトコルに準拠しています。

struct Array<T> : CollectionType {

}

そして、プロトコルの中で『できること』が決められています。

たとえば CollectionType であれば、要素の型が何であるか、最初と最後のインデックスを取得できること、サブスクリプトで要素にアクセスできること、要素を先頭から順番に取り出せること、といった『複数の要素を集めて扱える型として必要な最低限の機能』が決められています。

protocol CollectionType {

	typealias Index : ForwardIndexType
	typealias Generator : GeneratorType
	
	var startIndex: Self.Index { get }
	var endIndex: Self.Index { get }
	
	subscript (position: Self.Index) -> Self.Generator.Element { get }
	
	func generate() -> Self.Generator
}

これらに沿った実装を Array で行うことで、複数の要素を集めて扱える型という根本的な動きが決まります。


根本的な動きが決まれば、あとはそれに沿って必要な機能を追加していくことで、自由度の高い型の設計が可能です。

たとえば、最初の要素と最後の要素を取得できるようになっているので、要素の数を取得する count プロパティも次のようにして簡単に Array に実装できます。

extension Array {

	var count:Int {
	
		return distance(self.startIndex, self.endIndex)
	}
}

このように、型ごとに機能を実装していく方法は Objective-C などのオブジェクト指向言語では一般的なことでした。

プロトコル主体の機能設計

ただ、複数の要素を集めて扱える型ということが CollectionType プロトコルで決まり、その機能を使って数を数えるメソッドを実装しているのなら、同じように CollectionType に準拠した各型ごとに count メソッドを実装するのは非効率です。

そこで Swift 1.2 までは、ジェネリック関数を使って型に縛られない機能が実装されていました。

func count<T:CollectionType>(x: T) -> T.Index.Distance {

	return distance(x.startIndex, x.endIndex)
}

このようにすることで、どんな型であっても CollectionType にさえ準拠していれば使える関数をつくることができました。型ごとに機能を実装する必要はありません。

たとえば Array と String が CollectionType に準拠していますが、どちらでも次のようにして、同じ count 関数を使えます。

let array:Array = [ 1, 3, 5, 7 ]
let string:String = "ABCDEF"

count(array)
count(string)

所有する型による動きの切り替え

Swift では型の中に型を定義できるようになっていますが、その型の性質に応じて機能を細かく定義できるようになっています。

たとえば CollectionType で扱う要素の型が大小で比較可能だったとき、並び替えを行う sorted 関数を実装するということも可能です。

func sorted<S:CollectionType where S.Generator.Element : Comparable>(source: S) -> [S.Generator.Element] {

}

このような関数を定義すると、引数 source に渡す型が CollectionType に準拠していて、さらにそれが扱う要素 (Generator.Element) の型が Comparable に準拠している場合に限って利用できる関数になります。

Comparable プロトコルには大小比較に必要な機能が定義されているので、この機能を使って要素を小さい順に並び替えた新しい配列を作ることができます。

ジェネリック関数に見る問題点

このように、型にとらわれない実装ができたり、型がもつ型の種類を想定したプログラミングができる便利なジェネリック関数ですが、問題に感じる点もわずかに見られます。

大域関数はコード補完しにくい

まず、これまでの Objective-C 的な流儀では、配列の数を数えるときには array.count のようにして書くのが一般的でした。それが Swift 1.2 では count(array) と書くのが、プロトコル主体を意識したときの理想的な書き方になります。

それでもやっぱり、見やすさとしては array.count が意味合い的には分かりやすいように思います。

array.count なら『配列の数』と流れで読めますが、count(array) だと『数をかぞえる、配列の』みたいになって、対象を後に書かないといけないため、先に目的を意識しないとコードを書く順番に手戻りが生じてしまいます。


そしてこの性質は、プログラミングする人だけでなく Xcode も影響を受けます。

プログラミングは『何かの値に対して何かの処理をする』ことが多く『何かの値』を記述したときに、それで目的の処理をするために使う機能がどれかを素早く知れると便利です。

そこで Xcode では『何かの値』を記述したときに、それで使える機能を列挙して選べる「コード補完」という機能が提供されているわけですが、大域関数を先に書く方法だとそれがうまく働いてくれません。

先に機能を書かないといけないわけですから、つまり人が、補完より先にやりたいことを実現する機能を知っていないといけません。

機能が重複している

そのせいか、Swift 1.2 までは Array にも count メソッドが実装されていて、それによって count(array) でも array.count でも、どちらの書き方でも実行できるようになっていました。

これは count だけでなく、map, flatMap, first, last, isEmpty, reverse など、頻繁に使われる機能は大域関数だけでなく、型そのものにも実装を持たせることで実現されています。


これでは、どちらかの実装がもったいない感じです。

そんな様子を眺めていて、Swift ではプロトコル主体に考えるとしっくりきますし、それに 2 つの書き方があると設計の本質が見えにくくなるので、いっそのこと型へのメソッド実装は最小限にとどめて削除してしまったほうがきれいになるんじゃないかなと思ってました。

それが、Swift 2.0 ではもっとずっと理想的な方法で解決が図られることになりました。

プロトコル拡張による実装

それを解決する手段として、Swift 2.0 では『プロトコル拡張』が用意されました。

プロトコル拡張を使うと、プロトコルに実装を追加できます。これによって、どんな型でも使える機能を、プロトコルに準拠させた型そのものに持たせることが可能になります。

extension CollectionType {

	var isEmpty: Bool {
	
		return self.count == 0
	}
	
	var count: Self.Index.Distance {
	
		return distance(self.startIndex, self.endIndex)
	}
	
	var first: Self.Generator.Element? {
	
		return self.isEmpty ? nil : self[0]
	}
	
	var indices: Range<Self.Index> {
	
		return self.startIndex ..< self.endIndex
	}
}

これによって、大域にジェネリック関数を用意しなくても、しかも型ごとに機能を実装しなくても、そのプロトコルが想定している使い方を支援する機能を、準拠させたすべての型に実装されます。

こうすれば、たとえば CollectionType に準拠した MyStruct 型があったときに、それでもすぐにこれらの機能が使えるようになります。

let value = MyStruct()

value.count
value.indices
value.sorted().first!

これらはメソッドやプロパティとして実装されるので、先に扱う値を書いて、続けて使いたい機能を記載することになります。そのため、コードの補完も適切に効きます。

丸括弧が重なる機会も激減するためパッとみてもみやすいですし、先ほども話したように『この値に、これをして、これをして、...』という流れで、意識どおりの流れでコードを書いて行けるので、コードの仕上がりもとても分かりやすくなります。

プロトコル拡張で定義したメソッドやプロパティは、プロトコルに準拠させた型で自由に実装することもできます。準拠させた先で実装しなかった場合に、プロトコル拡張で定義した動作が自動的に実装されます。

内包する型による動きの切り替え

そんなプロトコル拡張ですが、とてもよくできていて、これまでのジェネリック関数と同じように、内包する型にあわせて実装を追加できるようになっています。

たとえば CollectionType であれば、要素が大小比較可能だった場合に限って、並べ替えを行う sort 関数を実装するということも簡単にできます。

extension CollectionType where Generator.Element : Comparable {

	func sort() -> [Generator.Element] {
	
	}
}

このように extension で where 句を使って型の条件を明示します。

今回の例では CollectionType が持つ要素 (Generator.Element) が比較可能 (Comparable) ということを明記しているので、要素の順序比較をつかった機能を CollectionType に追加実装できます。

これにより『同じ型だけれど、それが扱う型によっては特別な処理を実装する』といったことが、とても簡単になります。

コード補完が見事に働く

そしてこのとき素晴らしいのが、先ほどの例で実装した sort 関数は要素が当然 Comparable に準拠しているときに限って使えるものになりますが、つまりそうではなかったときには「実装されない」という特徴があります。

つまり、たとえば『比較可能な構造体 OrderedValue』と『比較できない構造体 UnorderdValue』という 2 つのデータ型があったとすると、次のように型を定義するだけで、先ほど作った sort 関数が備わっているかどうかが変化します。

let oArray = Array<OrderedValue>
let uArray = Array<UnorderedValue>

// 比較可能な型を扱う配列でだけ sort メソッドが使える。
oArray.sort()

つまり、今までのジェネリック関数では「使えるかどうか」に着目されていたのが、今度のプロトコル拡張では「存在するかどうか」が着目されている感じです。


そしてこれが、コード補完にとても見事に生きてきます。

配列を格納した変数名を記述して、つづけてドットを打ったときに、配列が扱う要素が大小比較可能だった場合に限って sort 関数が補完候補に挙がってきます。

ジェネリック関数の場合、使おうとして関数名を書いたけれど使えなかったみたいなことがよくありましたけど、プロトコル拡張による方法なら、書く前に補完機能で把握できるところが嬉しい感じです。

これからの在り方

このようにプロトコル拡張は、これまでの Swift のジェネリックプログラミングの完成系と言っても良さそうなくらい、強力かつ効果的な実装の在り方のように感じます。

そんな、新たに登場したプロトコル拡張ですが、Swift 2.0 ではさっそくそれを最大限に生かした作りになっています。


それを象徴するかのように、それで不要になった大域関数を潔いほどにざっくり削除しているところに、Swift 2.0 のプロトコル拡張への本気度が窺えます。

たとえば count や isEmpty、first、indices、map など、多くのジェネリック関数が、プロトコル拡張による実装に移行され、大域関数からはそれらが削除されています。


ここから窺えることとして、これからはきっとプロトコルを定義して、それに対する機能は extension で実装していくことが自然なプログラミングスタイルになるのでしょう。

同様に、既存のプロトコルを想定した機能を実装したいときにも、既存のプロトコルを extension で拡張して実装していくことになるんじゃないかなって思えます。

これまでの Objective-C では、既存のオブジェクトを extension で拡張するのは避けるきらいがありましたけど、Swift 2.0 では既存のプロトコルを extension していくことが自然なスタイルになっていくように思えました。

プロトコル拡張に感じる懸念

魅力を見れば有り余るほどの利点を感じるプロトコル拡張ですが、わずかに気になるところもあります。

それは Swift 1.0 から導入された名前空間による区別が、プロトコルが要求するメソッドやプロパティの名前までには及ばないところです。


名前が衝突したときに、両方のプロトコルが同じ主旨の実装を求めているならいいのですが、違った場合には区別することができません。

たとえば decode という名前であれば、それが HTML モジュールの HTML をデコードするものなのか、Audio モジュールの Audio をデコードするものなのか、それとも MODEM モジュールのデコードなのかもしれません。

ひとたび衝突してしまえば、同時実装を諦めるしか手がなくなってしまうので、既存のプロトコルを拡張するのが主流になるとすればなおさら、ここをどう乗り切るかがひとつの鍵になりそうにも思えました。

そんなあたりが気になったので こちら で少し考えてみることにしました。