https://youtu.be/fnYY3aUCXMk
今回は、前回に「時間があったら話すかも知れない」と予告しつつも辿り着けなかった A Swift Tour
の最後の節、Generics
について眺めていきます。
こちらの話題はとても広大で、見渡そうにもどういう流れで見ていったらいいか難しそうなところですけれど、まずは Swift ツアーの流れに沿って Apple がどんなあたりに着目してもらおうとしているのか、そんな意図を窺いながらざっくりと見ていけたらいいなって思っています。どうぞよろしくお願いしますね。
—————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #75
00:00 開始 00:17 今回の着目ポイント 02:09 ポリモーフィズム 05:00 ジェネリクスの日本語表記 05:53 ジェネリクスの特徴 06:22 型安全 07:45 ジェネリクスの基本 08:56 総称関数 13:03 総称関数の使い心地 14:46 ジェネリクスが使えなかったとすると 16:14 総称型の制約 18:36 総称型に制約を加える 20:14 ジェネリクスの要所 21:05 ジェネリクスのバイナリ出力 22:12 C++ のテンプレートと特殊化 25:57 Swift のジェネリクス 27:47 Swift におけるジェネリクスの表現のしかた 28:58 型パラメーター 30:37 談笑 : Swift でのジェネリクスの扱われ方について 31:32 型パラメーターを使ってみる 34:29 型パラメーターに振る舞いを備えたいとき 36:13 型パラメーターが異なれば、違う型 38:13 型のオーバーロード的な存在 40:33 談笑 : Swift におけるジェネリクスの扱い(続報) 42:49 今日のおさらいと次回の展望 45:01 クロージング ——————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #75
はい、じゃあ始めていきますね。今日のテーマは、Swiftツアーの最後のセクションですね。とても難しい分野と感じるかもしれませんが、実際そうなんです。このSwiftツアーの中で取り上げられているジェネリクスは、その中でもほんの些細な一角に過ぎません。非常に少ないページ数で語られています。スライドにすると5ページ分ですね。非常にライトに記載されているので、逆に興味深いところでもあります。
説明するとき、どう話していこうか、流れが掴みにくい感じの分野だなという印象がありますが、Appleはとりあえずスライド5枚分にまとめてくれています。ジェネリクスを捉える上でまずこの辺りが大事かな、という見方もできると思います。教える側としても、どういうふうなところに着目してジェネリクスを語っていったらいいのか、入門的な入り口として参考になるかもしれません。ジェネリクスに関してAppleがどうまとめているかを見ると、勉強になる部分があると思います。
ジェネリクスについて具体的にどういう分野の話かというと、ポリモーフィズムですね。ポリモーフィズムのうちのパラメータ多相というものに当たります。パラメータを多相、多様に表現していく感じです。ポリモーフィズムと言えばオブジェクト指向でおなじみのサブタイピング多相が主に思い浮かびますが、他にもジェネリクス、アドホック多相、オーバーロードもポリモーフィズムの一つです。
ジェネリクスとオーバーロードが全然似ていないと思うかもしれませんが、それでも価値観を持って見ていくと確かにオーバーロードっぽい雰囲気も見せる部分もあります。サブタイピング多相にも似ていない気がしますが、確かにそう感じる部分もあります。要は、今までの基礎的な知識や、オブジェクト指向でジェネリクスがなかった頃の言語の知識を動員すると、ジェネリクスを理解しやすくなるという印象です。
ただ難しいことをいろいろ言いましたが、そこまで深く考えると逆に混乱する可能性もあるので、まっさらな状態からこれから話すことを見ると理解できるかと思います。いろんな見方で、自分に合いそうな見方で見ていってください。
ジェネリクスという言葉についてですが、日本語だと「総称型」という言葉があります。汎用ジェネリクスという訳もあるので、その両方の訳し方があるようです。ジェネリクスの特徴としては、型を抽象化してコードの再利用性を向上させるとともに、定的型付け言語の型安全性を維持できる点が挙げられます。コードの再利用性を向上させ、型安全を維持できることが大きな特徴です。
Objective-Cにも途中からジェネリクスの構文が入ってきて、Swiftが登場してからコンパイルタイムに多くの検出ができるようになりました。その頃の記憶が残っている方は、便利になったと感じたかもしれませんが、書き方がややこしく感じることもあります。ジェネリクスはObjective-Cでのコンパイルのためのヒントでしかなく、ランタイムになるとその情報は必要なくなるので、コンパイルタイムの整合性が大事なメリットになります。
本題に入っていきましょう。まず、ジェネリクスはどういったものかをざっくりと捉えるためのサンプルコードが「The Swift Programming Language」に記載されていたので、それを見ていきましょう。まず、山括弧で括った名前が総称関数や総称型を作る例です。 とりあえずこれですね。この山括弧で括った名前、これを添えることによって総称関数、これね、MakeArray
という関数が総称関数になります。この時のアイテムというのが総称型、これを作るって言っていいのかな。ここはちょっと曖昧です。あまりこういう言い方はしないので、とりあえずこういうふうに作っていけるよというのがまず一つの総称関数です。
とりあえず、この他にもいろんなジェネリクスの表現方法がありますが、まず総称関数をじっくり見ていきましょう。関数定義、この総称型を指定するところを無視すれば、普通の関数定義と同じ書式です。それで普通の関数と大きく違う特徴としては、ここの型というか総称型、これを記述することでその型はとりわけ特定されていないんだけれども、その型があるものとして使っていけるよというふうになって、これが汎用化する特徴ですね。
型はとにかく何でもいいですが、ここではアイテムと呼びましょうか、みたいなそういった表現です。それで実際にこの関数の中ではこの型があるものとして使っていって、それを使っていろいろやっていき、コードが完成します。今回の場合はうまくいっています。アイテムの配列でアイテムを受け取ってアイテムの配列の変数にアペンドしていきます。だから全く問題ありません。
こういうふうにアイテム型が何かわからないので、それを使って何かをするというわけにはいきませんが、今回の場合は配列の要素としてアイテムを使うことで、そのアイテムがどんな振る舞いができるかとかを全く関係なくコードを組み立てていけているって感じですね。あくまで今回の例では、アイテムの中身については触れていません。要は item.
みたいな感じで何かを読んでいるということはしないので、そういうふうにしてコードが組み立てていけて、それで MakeArray
関数を呼び出すときにも文字列リテラルだから文字列型 String
を渡しています。そうすると実際に MakeArray
が呼ばれるときにはこのアイテムが String
として見なされて実行されるという感じです。
もし repeating
の後にデータ型を渡したとすると、アイテムはデータ型と見なされてこの処理が実行されます。こんな感じですね。そして repeating
に String
を渡す場合もあれば、データを渡す場合もあるみたいな、いろんな型でいろんな場所で呼び出しているみたいなことがあったとすると、 MakeArray
がオーバーロードみたいにアイテムが String
型のときの記載とデータ型のときの記載の両方の関数を持つみたいな感じになる。
この辺り話が長くなったから、プレイグラウンドで確認しましょうかね。要はどういったことを話していたかというと、このコードね、さっきスライドに出ていたやつ。これがまず実行できるわけです。MakeArray
でほら、こうやって配列ができました。これをこっち押せばいいのか、こうやって配列がちゃんと4つありますね。スクロールできるのか、なんか微妙な罠ですねこれ。まあいいや。
こういうふうにできたり、コード1個しか書いていないのにデータ型じゃなくて Double
型にしようかな。インポートいらないのでね。そして書いてあげると、こっちはね、ちゃんとこうやって。なんだこれすごいね。これはすごいな、どういうことだ。フォントを大きくしてるとダメなのかな。まあどうでもいいか。
とりあえずね、フォント小さくしてみると全然関係ないね。なんかバグってるね、まあいいか。こういうふうに Double
型だろうと String
型だろうと、この1から10行目、このねコード1個だけで実現できているっていうね。こういう汎用化をするのがジェネリックスの一般的なあり方ですね。とても基礎的なジェネリックス。
もしジェネリックスが使えない関数、じゃない言語だった場合には、こういう書き方ができない都合、アイテムを例えば String
型として受け取ってこれを実際に使って組み立てていくパターン。この関数と、あともう1個ね、今回の場合だと Double
型か、Double
型を受け取ってそれで Double
型の配列を返す、こういう関数。こういうものを実装してあげて初めてさっきの23行目と24行目、この2つのコードが実行できるようになるっていうね、こういう感じ。
これが一つの型パラメータを使うことでまとめあげられるよっていうのが、とても大きいメリットです。ジェネリックスに慣れてない方がいたら、まずここから練習していくといいですよ。 とりあえず、ジェネリクスの基礎的なところは掴めているかなと思います。ところで、アイテム型がどんな型か明記してないので、実際にどんな型になっているのか気になるかもしれません。これがコンパイルタイムではどうなっているのか、ソースを書いている時点でどうなっているのかを見てみましょう。
例えば、以下のようにコードを書けば分かるかなと思います。
func exampleFunction<T>(item: T) {
// アイテムの型は T
print(item)
}
この場合、item
はジェネリック型 T
として認識されています。つまり、言語から見て、この型は T
(任意の型)です。しかし、この T
が具体的に何ができるかというと、何もできない状態です。それは T
が総称型であり、インターフェースを持っていないからです。
そのため、ジェネリクスの中でできることは、item
そのものを使うことだけです。しかし item
は総称型であるため、APIを呼び出すことはできません。これを使わないで、表現できるコードを書いていく必要があります。これがジェネリクスの実装時の特徴です。
もし何を言っているのかよく分からないと感じたら、T
は Any
型だと思ってください。その中でどんなコードを書いていけばいいかが想像できるかと思います。
じゃあ、APIを実際に使いたいときはどうしたらいいのか。プロトコルが役に立ちます。例えば、プロトコル A
があって、これがAPIとして action
を備えているとします。
protocol A {
func action()
}
func exampleFunction<T: A>(item: T) {
// itemはプロトコルAに準拠している
item.action()
}
このように、必要に応じてプロトコルをもとにAPIを規定し、そのプロトコルでオープンにしているAPIのみを使って関数の実装を組み立てることができます。インターフェースの概念に馴染みがある人にとっては理解しやすい部分かと思います。
Javaなどの言語もこのようにジェネリクスを組み立てていく基本的な考え方があります。基本的にはこのように考えると分かりやすいです。しかし、応用が効くので難しく感じることもあるでしょう。頭がこんがらがったら、ジェネリクスの基本に立ち返ってください。
ジェネリクスは「何でもいいけど、ある型があって、その型がどういうインターフェースを提供しているかをプロトコルで味付けして、処理していくもの」と理解しておくと良いでしょう。
ちなみに、どういったふうにバイナリになっているかというところは自分は把握していないのですが、言語によって異なります。例えば、C++の場合、ジェネリクスはテンプレートという概念で実装されています。
template <typename T>
void exampleFunction(T item) {
// itemはジェネリック型T
std::cout << item << std::endl;
}
C++の場合、テンプレートをヘッダーに書くとライブラリを作るときにそのソースコードも提供されます。これにより、コンパイル時に新しい型でビルドすることが可能です。しかし、テンプレートの使用によってビルドが遅くなることがあるので、注意が必要です。
最後に、これら基礎を押さえておけば応用の理解が進みやすくなると思います。それでは、次に進みましょう。 すみません、私もかなりうろ覚えです。
ちょまど
なるほど、テンプレートの特殊化って、そういえば聞いたような記憶がありますけど、何でしたっけ?ジェネリック…なんかそんなような名前だったような。
ちょまど
そうそう、なんかありましたよね。特殊化っていう言葉は聞き覚えがあります。自分もC++をずいぶん前に触らなくなっちゃったので、忘れちゃったんですけどね。
ジェネリック
私もそうですね。もうずいぶん触らなくなって長いので。でも結構、特殊なことがいろいろC++のジェネリックにはあったような思い出がありますね。
ちょまど
そうなんですよね。C++のジェネリック…いや、テンプレートをご存じの方、他にもいらっしゃると。それとSwiftのジェネリックは結構違ってて、C++のテンプレートはダックタイピングなんですよ。ダックタイピングっていうのは、その場その場であるかどうかっていうのをバータリ的に確認していく感じ。だからマクロ的なんですよ、テンプレート。
それで特にマクロって#define
ってやって、型は指定しないでとにかくテキストベースで置き換えていって、最終的に辻褄が合えばオッケーっていうのがC++のテンプレートの発想です。
Swiftのジェネリックは一見似たような感じなんですけど、あらかじめプロトコル等でしっかりと制約をつけた上で、その制約がちゃんと辻褄が合うようにコードを書いていくっていう感じです。だから、すでに道をしっかり敷いてからやるか、いきなり突入していって最終的に整えばオッケーっていう二つの大きな違いがあります。
その辺りが、私がSwiftのジェネリックを使い始めてすごく戸惑ったところです。最初の頃、「ほとんど何もできないじゃん」と思ったことがありました。しかし、実際に使って慣れていくと、プロトコルで説明した上でその世界観の中でコードを組み立てていく形のほうが、頭がこんがらがる要因がなくなったり、コンパイルエラーが非常に明瞭になったりして、いろんなメリットがあります。
最終的にはSwiftのほうが効率よく気持ちよくコードが書けると言う印象を持ったので、最初自由度がなくて難しいなと思った方がいたらまずはその業に従って慣れていくという気持ちで受け入れると、いい感じに学んでいけるかなと思います。
いろいろ長くなりましたが、ビルドの話をしていたのは、もしかするとSwiftはもうちょっと違うジェネリクスの表現をしているらしいという話を、以前Appleの人が教えてくれた気がするんですよね。ちゃんと理解できなかったので分からないんですけど、ちょっと特殊なバイナリの持ち方をしてそうな話をしていた気がするので、もしバイナリがどう生成されるかに関心を持っている人がいたら、そういったアプローチを調べてみるといろいろ発見があるかもしれないです。
とりあえず次へ行きますかね。ジェネリクスのスタイルの他に、型パラメーターという持ち方もあります。こっちのほうがポリモーフィズムで見たときのパラメーター多様性に関係している気がしますので、イメージが寄せやすいかなと言う気もしますけれど、とりあえず。
これジェネリクスの醍醐味の一つで、クラスや列挙型、構造体でも型パラメーターを使えますよっていうことです。さっき関数名の横に書いてあった山括弧 <T>
に対して名前をつけると、とにかくこの型が分からないけどラップル(ラップされる)型があって、それを中で使えるよっていう話です。
じゃあ、この型パラメーターの方を実際に見ていこうかなと思うんですけど、画面に出ているのはSwift標準で提供されているオプショナルと同じ表現というのかな。こうやって列挙型の付属値、違う、関連値にも使えるし、ストラクトとかでもちゃんと使える、その辺りをちょっと見ていきますか。
その前にコメント。Swiftだとバイナリ生成時にジェネリクスのスペシャライゼーションとしてそれぞれ必要な具体化した関数型を作っちゃう。そうなんだ、じゃあC++のやつと、自分が把握しているつもりになっているC++のテンプレートと同じ感じなのかな。何か違うっていう話をしてた気がするんですよね、Appleの人が。まぁ、いいか。 とりあえず、一応気に留めておきましょう。なるほど、やっぱり作っちゃいますよね。作っちゃうぐらいしかやり方があまりないものです。実行速度とかも考えると、まあいいや。
とりあえず、型パラメータについて説明します。どういうものかというと、例えばよくあるのがスタックを作るときですね。ジェネリクスがないと中で扱うものを型で規定しないといけません。だからプライベート変数にして、それでバッファーにどんな値を蓄積するかを明示する必要があります。
ジェネリクスが使えない頃は、何でも入る型にして組み立てるしかできなかったわけですが、ジェネリクスを使うことによって非常にシンプルにコードを書けるようになります。正確にいうとですね。
今回は例として、プッシュだけを取り上げます。例えば、型 T
があって、具体的に名前をつけるかどうかは学ぶ上ではどうでもいいですが、簡単に T
としておきます。何らかの型 T
があって、それをインサートしていく感じです。それによって、例えば values
という変数の場合、Int
型を扱う、または文字列型 String
を扱うスタックを作ることがシンプルに書けるようになります。
型パラメータの基本はこれで終わりです。今のままだとAPIが何もないので、振る舞いを想定したいときにはプロトコルを使うことができます。例えば、型 T
があって、これがどんな型かは分からないけど CustomStringConvertible
に準拠しているとする。そうすると、受け取った T
は少なくとも CustomStringConvertible
のAPIを持っているということになります。
例えば、内部バッファが文字列で value
の description
をタッグに入れるコードを書きたいとしましょう。こういう書き方ができるようになることで応用が広がっていきます。これが型パラメータの話です。
さて、もう一つプレイグラウンドで説明したいことがあります。例えば、10行目と11行目では型が完全に違います。したがって、仮にこのスタックが比較可能だったとしても、values
と lines
は型が異なるので比較できません。
例えば、次のようにスタティック関数 ==
を定義します。
static func == (lhs: Self, rhs: Self) -> Bool {
return true
}
こうすることで、スタックを Equatable
にしたとしても、values
と lines
は型が違うのでコンパイルエラーになります。型定義は一つですが、型パラメータごとに全く異なる型として扱われるのが大きな特徴です。 昔、自分がジェネリクスに初めて触れた頃って、つい同じスタック型じゃないかみたいなノリで捉えて混乱していたことがあった気がします。今どきはそんなに混乱しないかな、分かりませんけど。型は全然違うよっていうところを意識すると、また面白いところが見えてくるかもしれません。何かの足しにはなると思います。
例えばですが、さっきの関数はある意味関数のオーバーロードでしたけど、ジェネリクス型は、型のオーバーロードみたいな感じで捉えておくといいのかなと思います。要するに具体的にどういったことが起こりうるかというと、ジェネリクス型がなかったときには、スタック型の中でストリングを取ってディスクリプションとするためには、カスタムストリングコンバーティブル型との関係が必要でした。
ジェネリクスがない場合、異なる型のためにそれぞれの関数をオーバーロードして、たとえば次のように書く必要がありました:
func stackString() -> String {
// some implementation
}
そして、
func stackInt() -> Int {
// some implementation
}
ですが、ジェネリクスを使うことによって、非常にシンプルな書き方ができるようになりました。
また、ジェネリクス関数の実際のコンパイルに関する話ですが、どういうバイナリができるかというところのコメントがありました。調査結果によると、スペシャライゼーションはSIL(Swift Intermediate Language)のステップで行われるそうです。この場合、静的リンクがされるとのことです。そうでなければ、プロトコルウィトネステーブル経由の動的リンクになっているとのことです。
プロトコルウィトネステーブル、要は関数テーブルですが、この存在意義はダイナミックに呼び出し先を切り替える機能があります。それを使って動的リンクをしているらしいですね。この辺りに関心がある方は、いろいろと調べてみると面白いことがたくさん出てきそうです。バイナリを生成する側の知恵がたくさん詰まっているので、勉強にはなりそうですが、難しいかもしれません。とはいえ、すべてが難しいわけではないので、もしかすると良いことが分かってくるかもしれません。
あと、型パラメータの制約に関する話ですが、時間がなくなったのでこれくらいにしておきましょう。今日はこの型パラメータの基礎と、型パラメータを使うことでどのようなことができるのかについてお話ししました。次回はジェネリクスについてもう少し深く掘り下げていくことになりそうです。
今日お話ししたこと、ジェネリクス周りについての理解が深まっていれば良いのですが、もし何か分からないことがあれば、後でスライドをアップしておくので、じっくり見ていただければと思います。それでは、今日はこれで終わりにします。お疲れ様でした。ありがとうございました。