【Swift】ジェネリクスが使われているメソッドを理解する
大島 雅人
ジェネリクスが使われているメソッドが読めない
BondというSwiftで関数型リアクティブプログラミングをするためのオープンソースがあります。試してみようと思いREADME通りにコードを書いてみましたが、コンパイルエラーになってしまいました。。どこが悪いのか調べようと思ってライブラリ側のコードを眺め始めたのですが、まったく書いてあるコードの内容が理解できないという事態になってしまいました。これではいけないと思い読めるようになろうと思って勉強した結果を記します。もし、Swiftでジェネリクス部分がよく分かっていないという方の役に立てれば幸いです。
実際に読めなかったコードはこちらです。
public func map<S: Dynamical, T, U where S.DynamicType == T>(dynamical: S, f: T -> U) -> Dynamic<U>
もうどこからどこが引数でこいつが何を返すのかがよく分からない。。map
のあとの<S: Dynamical, T, U where S.DynamicType == T>
は、ジェネリクスということは分かっているんですが、:
がついてたり、where
まで出てきちゃって何がなんだかさっぱりという感じでした。
ジェネリクスへの誤解
この勉強を始めるまでジェネリクスは、Array<Int>
やDictionary<String, Int>
のひし形括弧の部分で、Arrayの中身が全てIntである、DictionaryのキーはStringで値はIntであるというように、格納する値を制約するためのものぐらいに僕は思ってました。さっきの関数を見たときにS、T、Uとはなんだろう?どこで定義されているんだろう?そしてどうやってそのパラメータを渡しているんだろう?と分からないことだらけで思考停止していました。
そして、いい説明記事はないかとあさっていた訳ですが、Apple公式ドキュメントのGenericsの項を読んでみると、Generic Functionsと、Generic Typesがあるということが分かりました。さっき読めなかったコードは関数の部分なので、Generic Functions(ジェネリクス関数)ですね。Array<Int>
はGeneric Type(ジェネリクス型)なのでこの時点で既に認識が間違っていました。ということでまずは、ジェネリクス関数について調べていきます。
ジェネリクス関数について理解する
Appleの公式ドキュメントに載ってる例(2つの値を交換する関数)がとても分かりやすいので、それを引用します。
// 1. Intしか交換できない
func swapTwoInts(inout a: Int, inout b: Int)
// 2. Stringしか交換できない
func swapTwoStrings(inout a: String, inout b: String)
// 3. 好きな型のものを交換できる
func swapTwoValues<T>(inout a: T, inout b: T)
1つ目と2つ目は指定された型のものしか受け取れません。では、この機能を他の全ての型についてメソッドを書くのか?というのは気が遠くなる作業です。そこで、3番目のジェネリクスを使った関数が出てくるわけです。T
というのは他の言語でも同様ですが慣例的にT
と表しているだけでT
という型は宣言されているわけではありません。
そして関数を呼び出すときの書き方ですが、Array<Int>
やDictionary<String, Int>
のように型を指定しなくていいんだろうかと思ってしまいませんか?僕は思ってしまっていて混乱していたんですが、それはジェネリクス型のときの話でした。ジェネリクス関数の場合はswapTwoValues(a, b)
でよく、swapTwoValues<String>(a, b)
のようにジェネリクスの型を指定する必要はありません。
ただ、ここで一つ疑問でどんな型でもいいっていうなら、以下のようにAnyObject
使えばいいんじゃないかと思いませんか?僕は思ったんですが、これだとaとbで異なる型の変数を入れられてしまいますよね。こういう点でジェネリクスは複数の問題を解決する訳です!
// 例えば、aにInt、bにStringを入れられてしまう!
func swapTwoValues(inout a: AnyObject, inout b: AnyObject)
ここでもう一度読めなかった最初の関数を読んでみる
ひとまずここまでの知識で先ほどの関数を読んでみます。
public func map<S: Dynamical, T, U where S.DynamicType == T>(dynamical: S, f: T -> U) -> Dynamic<U>
map
という名前のメソッドである(public func map
の部分)- S型のdynamicalとT型の引数をとってU型を返す関数fを引数にとる(
(dynamical: S, f: T -> U)
の部分) - 内部でU型が使われるDynamic型を返す(
-> Dynamic<U>
の部分)
ここまでは理解できましたが、<S: Dynamical, T, U where S.DynamicType == T>
の部分がまだ分かりません。ただ、この部分もAppleの公式ドキュメントを読み進めていくとまさにこのことが型制約の説明のところに書いてありました。
型制約(Type Constraint Syntax)
まずは、型制約の説明です。これも公式ドキュメントの例が分かりやすいので引用します。
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// function body goes here
}
こんな風に引数にsomeTと、someUを受け取る関数がありますが、引数部分の前に、<T: SomeClass, U: SomeProtocol>
が書いてあります。まさに、この部分が引数の型制約を表しています。この部分を日本語にすると以下のようになります。
- someTは
SomeClass
のサブクラスである必要がある - someUは
SomeProtocol
に適合している必要がある
SomeClass
とSomeProtocol
がクラスなのかプロトコルなのかというのは表面上は分かりませんが、一般的に命名規則で分かるようになっています。
さらにもう一度読めなかった最初の関数を読んでみる
では、ここまでの知識でもう一度。
public func map<S: Dynamical, T, U where S.DynamicType == T>(dynamical: S, f: T -> U) -> Dynamic<U>
map
という名前のメソッドである(public func map
の部分)- S型のdynamicalとT型の引数をとってU型を返す関数fを引数にとる(
(dynamical: S, f: T -> U)
の部分) - 内部でU型が使われるDynamic型を返す(
-> Dynamic<U>
の部分) - S型はDynamicalプロトコルに適合している必要がある(
S: Dynamical
の部分)
さっき分からなかった、S: Dynamical
の部分が読めるようになりました。ただ依然として、U where S.DynamicType == T
の部分が分からないままです。Sは型のはずなのになぜドットで接続できるのか?、そして、UとSとTの関係はどうなっているのか?という点が僕は分かりませんでした。ということでさらに公式ドキュメントを読み進めます。
関連型(Associated Types)
まずは、U where S.DynamicType == T
のうちのS.DynamicType
の部分です。型にドットつけるなんてどういうことだろうと思いましたが、公式ドキュメントの関連型という項で説明されていました。
関連型というのは、プロトコルでジェネリクスを使いたい場合に使うもののようです。プロトコルでジェネリクスが使いたくなる場面のサンプルコードを引用します。
protocol Container {
typealias ItemType
mutating func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
このサンプルコードでは、Container
がプロトコル名、typealias ItemType
が関連型です。そもそも、プロトコルでジェネリクスを使う場合は、protcol Container<T>
と書けないのか?と思うんですが、それができないようです。(なんでできないかは言葉では説明できないんですが。。)
とにかく、その関連型についてはドット接続で表現できるということです。まさに、S.DynamicType
の部分ですね。では、実際に読めなかったライブラリのコードBond.swiftでDynamical
プロトコルを見てみましょう。
public protocol Dynamical {
typealias DynamicType
func designatedDynamic() -> Dynamic
}
おおーー、typealias DynamicType
がありましたね。つまり、S.DynamicType == T
の部分は、Dynamicalプロトコルに適合しているS型の関連型DynamicType
の型はTと同じ型である。ということを表しているわけですね。これであと残すは、where
の部分だけになりました。
Where Clauses
これもきちんとAppleのドキュメントに書いてありました。
僕はてっきり、U where S.DynamicType == T
のように、whereはUにかかってるものだと思ってたんですが違いました。ジェネリクスの全体にかかってました。
つまり、S: Dynamical, T, U
とwhere S.DynamicType == T
に分けて考えるということですね。whereはUにかかってるんじゃなくて、S、 T、 U全体にかかっている。まさにSQLのwhere句のような考え方ですね。S、 T、 Uという型を利用するが、ただし、SのDyanamicTypeの型はTの型と同じでなければならない。ということを制約しているわけですね。そして、U型には何の制約はかかっていません!
まとめ
これらを総合すると以下のようになるわけです。やっと読めました!!
public func map<S: Dynamical, T, U where S.DynamicType == T>(dynamical: S, f: T -> U) -> Dynamic<U>
map
という名前のメソッドである(public func map
の部分)- S型のdynamicalとT型の引数をとってU型を返す関数fを引数にとる(
(dynamical: S, f: T -> U)
の部分) - 内部でU型が使われるDynamic型を返す(
-> Dynamic<U>
の部分) - S型はDynamicalプロトコルに適合している必要がある(
S: Dynamical
の部分) - S型の関連型であるDynamicTypeの型はTと同じ型である必要がある(
S.DynamicType == T
の部分)
なんとなく見過ごしてきたジェネリクスですが、ライブラリや共通処理を書くときには使えるような気がします。JavaやScalaに慣れ親しんできた方には当たり前の内容だとは思いますがiOSにしか馴染みがない人にはちょっと難しい概念なんじゃないんでしょうか。僕も、Swiftはまだまだ使いこなせていませんがこれからも精進していきたいと思います。
ちなみに、今回調べたBondについて【第14回】potatotips(iOS/Android開発Tips共有会)でLTをします!人生初のpotatotipsなのでちょっと緊張していますw発表内容はこちらです。