これはSwift Tweetsの発表をまとめたものです。イベントのスポンサーとして Qiita に許可をいただいた上で、このような形(ツイートの引用)で投稿しています。
こんばんは。たるのんです。博多でSwiftを書いているなめくじです。美しい型とコードを愛しています。RxSwiftにちょっとだけコミットしています。 #swtws
— @tarunon
Swiftはとても柔軟かつ厳格な型の宣言が可能な言語です。上手に型を使いこなすことで、美しいプログラムの世界を実現することが出来ます。しかし、しばしばコンパイル出来ないプログラムを書いてしまうことがあると思います。 #swtws
— @tarunon
思い描いた美しい世界をあと一歩のところで実現できなかった。今日は、Swiftがコンパイル出来ない型とその原因、そして、それに対するWorkaroundを幾つか紹介します。幾つかの例では具体的なユースケースも合わせて紹介します。 #swtws
— @tarunon
1. Sequence型の変数を作りたい #swtws
— @tarunon
Swiftではassociatedtypeを含んだprotooclの変数を作ることが出来ません。SequenceもIteratorとSubSequenceの宣言があるため、変数として作ることは出来ません。 #swtws
— @tarunon
これに対するWorkaroundはとても有名で、type-erasureと呼ばれています。protocolが持つ全ての機能を変数として宣言することで、動作はそのままに、一つのstructとして表現することができます。 #swtws
https://gist.github.com/c828bdf9b7b015172ac566789d50fa5b
— @tarunon
Sequenceのtype-erasureはAnySequenceで、Swift標準ライブラリの中で提供されているので簡単に挙動も確かめることができます。 type-erasureは広く使われていますが、万能では無く、あくまでもWorkaroundです。 #swtws
— @tarunon
type-erasureは任意のprotocolをstructとして実体化したものです。もしprotocolがstaticな変数や関数を持ってしまっている場合は、それらは使うことができません。 #swtws
— @tarunon
また、type-erasureにprotocolを渡した場合、元々の型としての情報は、共通のprotocolとGenericsを残して全て消滅してしまいます。例えば型によって分岐するようなプログラムは、動かないでしょう。(最もそれ自体はあまり良くないですが) #swtws
— @tarunon
associatedtypeを持ったprotocolが変数として利用できないのは、エラーメッセージ(can only be used as a generic constraint)からも読める通り、今のところは言語仕様です。 #swtws
— @tarunon
しかし、GenericsManifestoでは記法が言及されていますし、将来のSwiftでは、もしかしたら可能になるかもしれません。 https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md#generalized-existentials #swtws
— @tarunon
日本語では以下の記事が参考になります。より詳しく知りたい方はぜひ。 http://qiita.com/omochimetaru/items/b41e7699ea25a324aefa http://qiita.com/koher/items/88f6d7a9c9dd2d8c25a7 #swtws
— @tarunon
2. 循環した型を作りたい #swtws
— @tarunon
型Aが型Bを生成し、型Bが型Aを生成する、という循環関係をprotocolで書こうとすると、ある条件下でコンパイル出来なくなります。 #swtws
https://gist.github.com/67cda3c51cc7a9e7c058266ea3784feb
— @tarunon
例えばPaginationRequestはこのパターンに該当します。 前提のRequest型は以下のものを考えます。※参考 APIKit(https://github.com/ishkawa/APIKit) #swtws
https://gist.github.com/0ea7928fd5f8baf48b091450a796979a
— @tarunon
Paginationを以下の3つに分離して作ります。 1. Responseのパース(Requestの具体的な実装) 2. Pagination protocol 3. Paginationの実装 #swtws
https://gist.github.com/51fa5d3e149e1f58e2e4be53dd8b0c06
— @tarunon
このPaginationRequest型はコンパイルすることができません。エラーには、Type alias `Response` circularly references itself. と出てきます。最初に紹介した2番目の例と状況は同じです。 #swtws
— @tarunon
循環を解消しなければ、コンパイルは通りません。循環を解消する方法を考えてみましょう。 #swtws
— @tarunon
PaginatedResponseのGenericsをRequest型で束縛すると循環型ではなくなりますが、Request.Base.Responseが参照出来なくなります。なのでResponseを追加してあげます。 #swtws
https://gist.github.com/9a97e501ffc252988c0ebe08f7a302a5
— @tarunon
これでコンパイルが通るようになります。やりました。
…重複した型をGenericsとして渡すのはなんだか腑に落ちないです。もう少し頑張ってみましょう。 #swtws
— @tarunon
PaginatedResponseがnextRequestとして期待するものは、同じPaginatedResponseを返すRequestの筈。それさえ表現出来ていれば、使う上で困ることは無いでしょう。しかし、Request型をそのまま変数として使うことは出来ない。 #swtws
— @tarunon
我々にはtype-erasureがあります。それを使えば、変数としての表現が可能になりますね。Requestのtype-erasureとして、AnyRequestを考えます。 #swtws
https://gist.github.com/82b380bba2221612fb3965fe1a0cbcc3
— @tarunon
type-erasureを使うことで、循環型を紐解いたPaginationRequestです。 #swtws
https://gist.github.com/4b490f277ce2a93ad233ee0101f9d1af
— @tarunon
やりました。PaginatedResponseのGenericsもResponseだけになって、心なしかスッキリした気がします。つまり何をしたかというと、2つの型で発生していた循環関係を、1つの型に寄せたわけです。 #swtws
— @tarunon
ではこの循環型はどうしてコンパイル出来ないのでしょうか。最初のケースの挙動と、コンパイラの性質から推察するに、恐らく半分は仕様で半分はバグではないかなと考えています。 #swtws
— @tarunon
最初の例でコンパイルが通らなかったのは2番目と4番目の型です。このうち、2番目の型はコンパイルするためにお互いの型が必要です。参照し続けてコンパイルが完了しないのではないでしょうか。一方で4番目の型でコンパイルが通らないのはバグでしょう。どう見ても循環していません。 #swtws
— @tarunon
4番目の型についてはバグ報告があり、masterにマージされている、とあります。来るSwif3.1では、コンパイルできるようになっているかもしれません。 https://bugs.swift.org/browse/SR-3478 #swtws
— @tarunon
ついでにPaginationRequestもWorkaroundなしで通ってくれたらな~と願ってみたりしますが、ちょっと難しそうな気がします。 #swtws
— @tarunon
3. Generics(associatedtype)のサブタイプを指定したい #swtws
— @tarunon
何がやりたいかということの文章化が難しいです。gistを見てください。以下のコードはコンパイルに失敗します。 #swtws
https://gist.github.com/811dc4abe0eb4798a7c5b6fc0e3b4ec5
— @tarunon
UIKitがSwiftyでなくて辛いシチュエーションは無限にあって、Swiftでprotocolを書き直して使うことはよくあると思います。そしてその作業の途中で、先のようなケースに遭遇することが多い。 #swtws
— @tarunon
具体的な例を見ていきましょう。UIViewController Transitioningのカスタマイズは、最も辛いシチュエーションの一つです。これを改善していきたい。 #swtws
— @tarunon
Transitioningが辛いのは、 1. 型が安全ではない(UIViewController?) 2. 状況によってfrom/toが入れ替わる の組み合わせによって発生しています。 #swtws
— @tarunon
なので、ここを型安全にすればかなり楽になる。(最近は専らHeroが話題ですが今は目を瞑って下さい) 100行ちょっとのProtocol郡です。 #swtws
https://gist.github.com/ac7ae6e2786dd9be36d05a23d3915c02
— @tarunon
使い方は簡単で、AnimatedTransitioningを実装して、UIViewController.presentに渡すだけなのですが、実際に使ってみるとある問題に気が付きます。 #swtws
https://gist.github.com/10fc75da2c9d1dfd4bd04bbbe3bb4fde
— @tarunon
一見全て上手くいくように思えますが、うまくいきません。そしてこのコードは、MyAlertTransitioning.Presenting = ViewControllerにすれば、コンパイルできるようになります。 #swtws
— @tarunon
SwiftではGenerics(もしくはassociatedtype)のサブタイプとしてGenerics funcを制約することは出来ません。ただし、Optionalは例外として指定することなくサブタイプとして振る舞うことが可能です。 #swtws
— @tarunon
先にどうして動かないか、というところの解説をしていきましょう。SwiftのGenericsには、その型においてGenerics型が関数の引数なのか、あるいは変数(または関数の返り値)として振る舞うかについての記述がありません。 #swtws
— @tarunon
class Aとclass B: Aについて、それぞれの型で宣言したものが、お互いに安全かどうかについて考えてみましょう。 A型の変数をB型の宣言で扱うのは危険です。もしかしたら、B型ではないかもしれない。なのでswiftではas!を使うことになります。 #swtws
— @tarunon
対してB型の変数をA型の宣言で扱うのは常に安全です。では関数の引数となった場合はどうでしょうか。 fA=(A)->()とfB=(B)->()について考えます。 #swtws
— @tarunon
fA型の関数をfB型の宣言で扱うのは常に安全です。かならず引数にBが入ってくるわけですから、fA型の実装、つまりAとして扱う分に何ら問題はありません。 #swtws
— @tarunon
しかし逆に、fB型の関数を、fA型の関数として扱うのは危険です。引数にはAが入ってくるが、fB型の実装、Bであるとは限らない。 関係性としては、A⊃Bの場合、fB⊃fAとなるわけです。変数と関数の場合に、サブタイプの関係が逆転しているのがわかります。 #swtws
— @tarunon
Genericsでの宣言はAですが、内部の実装としては当然fAがあり得ます。そして仮に下記のようなコードが実現できると、関数内における型の関係について、安全性を証明することはできなくなってしまいます。 #swtws
https://gist.github.com/ac24d30b5e4a9cec68a9c36c3bd15a31
— @tarunon
Generics周りのサブタイプ、Swiftのサブタイプの扱いについては、日本語だと以下の記事がわかりやすいです。ご参考ください。 http://qiita.com/koher/items/972ab84283b2a0d3ce1b #swtws
— @tarunon
さて、コンパイルできない原因が解りました。原因がわかって、目の前に動かないコードがある。あとは、Workaroundを作るだけです。1つは、MyAlertTransitionigの実装を変更する方法です。 #swtws
https://gist.github.com/67619c7606909c613c431c64bb4b0a1a
— @tarunon
MyAlertTransitioning自体をGenericsとして作り変える。そうすればMyAlertTransitioningは任意の型についてinitできるようになるので、コンパイルが通るようになります。おそらく最もシンプルかつ強力なWorkaroundです。 #swtws
— @tarunon
2つ目、Optionalと同じ発想で、Genericsのサブタイプを行います。ただし、Optionalはコンパイラの助けを受けて暗黙的に型変換が行われますが、それ以外の型については明示的に書き換える必要があります。 #swtws
https://gist.github.com/f2a9a9622ce53db0c40d08922db54dbd
— @tarunon
このmap関数を使えば、明示的に書く必要があるはものの、コンパイルが通るようになります。 通常意図されるmapと動きや意味が異なるのは許してください。名前を考える時間が無かったんです(発表一時間前の屑) #swtws
https://gist.github.com/b8660ba12ecacacb48fba607c03c4ceb
— @tarunon
3つ目は、Tupleを引数に取るtype-erasureと、パラメータをTuple化するExtensionを作って、AnyAnimatedTransitioning(MyAlertTransitioning().fields)で動くというものを #swtws
— @tarunon
考えていたのですが、残念ながらコレは動きませんでした。というのも、SwiftではTupleについては、要素毎のサブタイプはチェックしておらず、以下の最小のコードもコンパイルすることは出来ません。 #swtws
https://gist.github.com/6f064f5819869bad8239f654f8098390
— @tarunon
Argumentは動くじゃないか!と考えるんですが、ArgumentとTupleは別物です。Argumentは1つずつ型がチェックされますが、Tupleが1つずつ型がチェックされることはありません。 #swtws
— @tarunon
じゃあSwift2.0時代にあったArgumentにTupleを与える記法ですが、実はあれもサブタイプはチェックされず、同様のことをしようとするとコンパイルが通りませんでした。 …(´・ω・`) #swtws
— @tarunon
Tupleに関しては、Swift-evolutionのmailinglistに話題があります。前向きな回答ですし、将来のSwiftでは可能になっているかもしれません。(Genericsの改善の方が早そうですがw) https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160215/010363.html #swtws
— @tarunon
Conclusion. #swtws
— @tarunon
さて、約20分間に渡って、Swiftの型と戯れてきましたがいかがでしたでしょうか? ツイートに対するコードの配分が多く、追うのが大変だったかと思います。 #swtws
— @tarunon
特に、「3. Generics(associatedtype)のサブタイプを指定したい」は、最初からコードが凶悪で追いつけなかった人が多かったのではないか、と少し反省しています… #swtws
— @tarunon
それでも、一連のツイートの中で、過去に諦めた型が実現できそうだ、というものが少しでもあれば、或いは単純に面白いと思っていただけたのであれば、やった価値があったのかなぁと思います。 #swtws
— @tarunon
このセッションの中でわからなかったことや、このセッションで期待してたけど紹介されなかった型があれば、是非リプライやリアル(博多)で会った時に教えていただけると嬉しいです。特に、後者は大好物なので、あれば今すぐにでも考えます。 #swtws
— @tarunon
僕にとってSwiftを書いていて最も楽しい瞬間が、型を作っている時です。同じように、型を作ることが楽しいと思える人が、増えていけば良いなと、願っています。 おわり #swtws
— @tarunon
追記(2017-04-16)
共変性と反変性に関してかなり強力なWorkaroundが出来上がったので追記です。
3. Generics(associatedtype)のサブタイプを指定したい
について、これでおおよそ解決出来るはず。
https://github.com/tarunon/Covariancable