Swift 2 シンポジウムでたっぷり談義を楽しんできました。
Swift 2 (& LLDB) シンポジウム
2015/06/28 に開催された勉強会「Swift 2 (& LLDB) シンポジウム」にお邪魔してたっぷり談義を楽しんできました。
こういう談義主体の勉強会っていいですね。
に開催された @k_katsumiさん 主催の勉強会『Swift 2 (& LLDB) シンポジウム』に参加させて頂きました。
http://realm.connpass.com/event/16556/
最初に参加者リストを拝見したとき、いつも大阪で Swift について興味深い話を聞かせてくださる方々ですとか、Web で広く知られる Swift 最前線を行く方々、持ち前の取り柄を生かして新世界を築かれている方々など、そうそうたるメンバーが揃っていて。
これはもしかして凄まじく厳かな世界が繰り広げられるのではとちょっと身構えてみたものの、実際はそんなことはなにひとつなく、和気藹々とみんなで楽しく議論できる空間が広がっていました。
新しく素敵な勉強会の形式
この会は、意外と希少な『議論』を主体にした勉強会で、発表者はそこそこに時間はたっぷりとって談義に集中できるようになっていました。
参加枠も、よくある発表者 (Speaker) と参加者 (Audience) に加えて、新たに議論に積極的に参加してくれる人 (Panelist) も設けられているところも斬新です。
実際、それがとても功を奏していた印象で、総勢 100 名という大規模な勉強会でありながら、はじめからとっても活発に意見交換が交わされて、終わってみればとってもたのしい時間を過ごすことができました。
そもそも掲げられた議論を主体にする場という共通認識もあってか、とても自然に和やかに議論が成り立っていたところもよかったです。
自分が発表してきたこと
さて、そんな勉強会で、自分も Speaker として参加させて頂きました。
議題は、Swift 2.0 に初めて触れたときに感じた「大域関数がごっそり削除された」感から、そこに見る Protocol Extension の魅力とそれにまつわる基本の紹介、そしてせっかくの議論の場なので、そんな Protocol Extension を積極的に使って行くべきか、みたいなところをお話させて頂きました。
発表の大筋は次のとおりです。
- Swift 1.2 では大域関数として用意されていたものが Swift 2.0 では Protocol Extension にとって代わられた印象。
- それなら大域関数を使ってきたのと同じように積極的に Protocol Extension を活用して行くべきと思うが、積極的に活用していって大丈夫だろうか?
この中で特に、積極的に利用する上で避けて通れない「機能名衝突の安全性」について、たくさんの意見を伺えて大満足でした。
衝突が考えられるポイント
Protocol Extension で衝突し得る気がするポイントとして、自分が掲げたのは次の 3 つでした。
- プロトコル名そのものの衝突
- 機能のシグネチャの衝突
- 型エイリアスの衝突
プロトコル名そのものの衝突については名前空間があるのでまったく問題なさそうです。
ただ、機能のシグネチャの衝突や型エイリアスの衝突については、名前空間を活かすことができないため、複数のプロトコルに型を準拠させた場合に、衝突してどうにもならなくなる可能性が考えられます。
ただ、これらの衝突について、機能のシグネチャ問題は自分のスライド (p55) の中でも述べたように、少しばかり気を使えばおおよそ回避する手立てはありそうです。
型エイリアスの衝突については、今のところ、自分は良い?衝突ケースが想定できず、他の方からもとりわけ「やばい!」という声も上がらなかったことから、ひとまずは積極的に使っていて問題に遭遇したら考える、みたいな形で決着した感じかなって受け止めさせて頂きました。
浮上した既定実装の衝突問題
ところでそんな談義の中で、プロトコルのディフォルト実装が衝突したときにどうなるのか、みたいな話に発展したのですけれど、もしかしてこれってクリティカルな事例になるかもしれません。
— あきお@大阪アプリ開発講座8/1 (@akio0911) 2015, 6月 28
まず、ディフォルト実装がなかった場合、複数のプロトコルで同じ実装を期待した程度では、それが即、衝突には結びつきません。
// プロトコルを定義します。既定の実装は与えません。
protocol PNGType {
func method() -> String
}
// プロトコルを定義します。既定の実装は与えません。
protocol JPEGType {
func method() -> String
}
// この場合、両方のプロトコルに準拠させても、
// 同じシグネチャの機能はひとつだけ実装すれば大丈夫です。
struct Picture : PNGType, JPEGType {
func method() -> String {
return "PICT"
}
}
これは、両方のプロトコルが「同じ答え」を期待するという前提で、ひとつを実装するだけで「両方にいちどに対応できる」という動きになるためです。
これについては、スライドの p57 で紹介しました。そしてもし、ここで「違う答え」を期待していたとするときの回避案については p58 と p59 で紹介しています。
同じシグネチャで既定の実装を持ったときの衝突性
この印象に自分が捕らわれ過ぎていて、談義の中で気づくのが遅れたのですが、ここから発展して議題に挙がっていた「両方をディフォルト実装していたときにどうなるか」という視点になると、ときに深刻になりそうかなって、思いました。
// プロトコル定義に機能を記載したとします。
protocol PNGType {
func method() -> String
}
protocol JPEGType {
func method() -> String
}
// 記載した機能の、既定の実装を両方に与えます。
extension PNGType {
func method() -> String {
return "PNG"
}
}
extension JPEGType {
func method() -> String {
return "JPEG"
}
}
// 同じ既定の実装を与えられたプロトコルに準拠させようとするとエラーになります。
struct Picture : PNGType, JPEGType {
}
// 実際に機能を使うコードでは、メソッド呼び出しが曖昧と指摘されます。
let pict = Picture()
pict.method()
このようにしたとき、実際に method を使おうとしたタイミングで、次のビルドエラーになります。
Ambiguous use of 'method'
全く同じメソッドが、複数のプロトコルから既定の実装として与えられている、要は「曖昧な実装になっている」ため、どれを呼び出したら良いのかがわからないということを主張しているのでしょう。
そしてこのとき面白いのが、構造体 Picture を両方のプロトコルに準拠させようとしているところで、不可思議な次のビルドエラーが発生します。
Type 'Picture' does not conforms to protocol 'PNGType'
Type 'Picture' does not conforms to protocol 'JPEGType'
既定の実装だけしかないのに、なぜ両方から「準拠できていない」と指摘されているのか。
これを見させてもらった最初は「Protocol Extension ってまだまだ甘い実装なのかな」と思ってみたりもしたんですけど、それからゆっくり考えていたら、なんとなく Swift の気持ちが分かってきました。
エラーの理由は曖昧性
まず、そもそもの大事なポイントなのですが、既定の実装を extension で添えるに当たって、その機能のシグネチャを protocol に宣言しておく必要はありません。
それはひとまずさておいて、じゃあ、どうしてこのような矛盾めいた不可解なエラーが発生しているかというと、根本は「曖昧な実装になっている」という指摘が影響していると思われます。
まずは Swift が構造体 Picture に、PNGType と JPEGType の両方で定義された同じシグネチャの既定の実装 (method) を両方とも適用します。
そこまでは良いみたいなのですが、それを実際に使おうとしたタイミングで、呼び出しで指定されたシグネチャからはどちらを使うか判断できず、最初に紹介した「Ambiguous use of 'method'」エラーが発生したものと思われます。
そして、もっとも不可解な PNGType と JPEGType のそれぞれに準拠できていないとするエラーがどうして発生するかというと、それは protocol 定義で「method の実装を求める」と書いてあるためです。
そして、それにちゃんと準拠できているかを判定しようとしたときに、シグネチャだけで検査をしても、既定の実装がどちらのプロトコル拡張が提供しているものなのか、判断できない様子です。そのため、PNGType については準拠できていない(曖昧なため準拠できているか確証が取れない)し、JPEGType についても同様、ということが起こっていると思われます。
これを解消する方法は、次のように、プロトコルの定義から method のシグネチャを消去します。
// プロトコル定義には、既定の実装の宣言は不要です。
protocol PNGType {
}
protocol JPEGType {
}
// 記載した機能の、既定の実装を両方に与えます。
extension PNGType {
func method() -> String {
return "PNG"
}
}
extension JPEGType {
func method() -> String {
return "JPEG"
}
}
// このようにすると準拠できていないとするエラーは解消されます。
struct Picture : PNGType, JPEGType {
}
既定の実装は、実装すべき義務を負う機能というより、自然に実装される機能ですので、上のように protocol 宣言の中には記載しないほうが良さそうです。
シグネチャが一致した既定の実装を区別して実行できることが判明
ところで「曖昧な実装」ということは、上手くすれば区別して呼びだすこともできるはずです。
そう思って、試しに as 演算子を使ってプロトコルにキャストしてみたところ、ちゃんと区別して呼び出せることが判明しました。
let pic:Picture = Picture()
(pic as PNGType).method()
(pic as JPEGType).method()
つまりこれで、今回の例で発生したビルドエラーは回避できたことになります。記述が面倒なのは目をつぶって、最悪、この方法を取ればメソッド呼び出し時の衝突は回避できると思って間違いなさそうです。
肝心なのは既定の実装の仕方で、protocol で必須項目として記載してしまうと、まったく同じものが別のプロトコルで既定の実装として提供されたときに、両方に準拠させることができなくなりそうです。それを避けるためにも、既定の実装を定義するときは「そのシグネチャを extension だけに記載して、protocol には記載しない」ということを心がけておくことは大事なように思えました。
それでも解消できないケース
ただ、そうして見てきた Protocol Extension ですけど、このディフォルト実装の衝突が避けられない可能性もあるかもしれないと思えてきました。
たとえば、既存のプロトコルに、誰もが思いつくような機能を追加実装したときです。
extension SequenceType {
func foreach(@noescape predicate:(Generator.Element)->Void) {
for element in self {
predicate(element)
}
}
}
このような実装を複数のモジュールで実装したとき、プロジェクト内で両方のモジュールをインポートしてしまうと、実際にこのメソッドを使おうとしたときに「Ambiguous use of 'foreach'」エラーになります。
そして、これを回避する方法があれば良いのですが、今のところは上手い方法がわかりませんでした。
とりあえず、こういう汎用的な便利機能は既存のプロトコルに対しては拡張せずに、大域関数として存在させておくのが適切なのかもしれません。スライドの p47 で紹介した total プロパティも、もしかすると衝突の可能性があるかもしれません。
つまり、既定の実装を伴う実装についても、スライドの p57 で紹介したような機能の衝突を避ける工夫がいっそう大事になってきそうです。
Protocol Extension の使いどころ(改
そんなことから、スライドの p66 で紹介した Protocol Extension の使いどころについて、但し書きを少し追加したくなってきました。
- ただし、既存のプロトコルを対象にした汎用的な便利機能(CollectionType に対する foreach など)の場合は、Protocol Extension ではなく大域のジェネリック関数で実装した方が良いかも?
積極的に使って行きたいことには変わりないのですが、こんなことは念頭に入れて使いどころを見極めていったらいいのかなって思いました。
そう思うと、詳しい事情は分かりませんけど AnyObject や Any に対して Protocol Extension ができないというのは、結果的に見て得策なのかもしれませんね。これらのプロトコルに対する機能とすれば、汎用的な便利機能が多くなりがちかもしれませんので。
Objective-C の extension ?
それと自分の発表の中で我ながら面白かったのが「Objective-C で extension するのは忌み嫌われてたよね」と話したときのことでした。
それに対して、岸川さんから「Objective-C は extension ではなくカテゴリです」みたいに軽やかに指摘され、思わず混乱してみたりしました。あれ、そうでしたっけ!みたいに確認してみるも、頭が不意を突かれたみたいで理解がおぼつかず、微妙に理解できないまま、話を進めたりしてました。
とにかく Swift にすっかりハマって Objective-C が記憶の彼方な自分に笑えました。
整理すると、Objective-C にはクラスを独自に拡張する技法があって、それが「カテゴリ」ということで良いんですよね。Swift で言うクラスへの extension が、Objective-C で言うカテゴリ拡張ということで、きっと大丈夫かなって思います。
そして、名前的に似たものに「クラスエクステンション」がある、ということも教えていただきました。
こちらも表向きはカテゴリと似ているけれど、カテゴリは実行時に動的に拡張するのに対して、クラスエクステンションは、厳密に言うと違うのかもわかりませんけど、いわゆる静的にクラスを拡張する仕組みのようです。
カテゴリほどの自由度はない代わりに、安全に使えるのがひとつの特徴になるようでした。
そんなところと照らしてみると、もしかして Swift の extension は、Objective-C のカテゴリと同じようなことができながら、クラスエクステンションみたいな安全性を併せ持つ、間の子みたいな仕組みと思ってみても良いのかなとも思えてきました。
そんな風に考えると、安全で使いやすくなったカテゴリと捉えることもできそうですし、Swift の extension ではさらに、クラスだけでなくプロトコルも拡張の対象になったことで、さらに面白いことに発展しそうな印象がします。
そしてそんな、自分が今回のスライドで尋ねたかった Objective-C のカテゴリ拡張の問題点として、ツイッターから次のような声が挙げられました。
なぜ、Objective-Cのカテゴリ拡張が危険だったか。。標準のメソッドを完全に置き換えてしまったり、かぶっていた場合、どちらが先に実行されるか不明?? #swift2symposium
— ずきゅ~んたん (@ZuQ9Nn) 2015, 6月 28
#swift2symposium カテゴリで辛かったのは、名前が衝突、片方の戻り値がFloat, 片方がCGFloatで64bitのシステムで時々クラッシュするケース。
— nori (@nolili) 2015, 6月 28
やっぱりけっこう危険な香りがしますよね。
それと比べて Swift の Protocol Extension からは随分と安心感が漂ってくるのは、やはりコンパイラによる安全性の保証が大きいのかなって思えてきました。Swift の型に対する extension も怖い感じがしないのも、それが物語っているのかもしれません。
同じテーマの発表っていいですね
さて、そんな発表を準備していった自分ですけど、大域関数という切り口から入ったこととはいえ、内容的には Protocol Extension の魅力に触れる内容でした。
この内容、主旨的には @ishkawaさん の「Protocol Extension」とほとんど同じ感じでした。
ただ、そんな風にテーマが重なることも良いものだなって思えました。
同じネタを複数の方から発表していただくと理解が深まる。 #swift2symposium
— Kaoru (@TachibanaKaoru) 2015, 6月 28
ほんとそう、同じテーマと言っても見る人にとって視点が違うので、説明の仕方も発見もそれぞれ違ってきますし、それでいて同じことを2回聞けるので、1回目で理解しきれなかった事柄を2回目だったら理解できるかもしれません。
だから、最初の記憶が消えないうちに自分も発表できたらいいなと思っていたら、岸川さんがしっかり順番を組み立てて、石川さんの直後に話をさせてくださいました。
そんな、同じテーマでしたけど、石川さんは Protocol Extension の「基礎〜実践」を扱い、自分は「事例〜基礎」を扱うみたいな、良い感じの住み分けがされてたかなって思えました。
そんな石川さんの発表の、実践的なところがとても興味深くて楽しかったです。
DataSource の実装例
たとえば DataSource の実装例 (p4 ~ p14) のところ。Swift 1.2 までの @objc をつけて optional な定義を求めるプロトコルの実装方法を、Swift 2.0 では Protocol Extension を使って実現するというアプローチが、とてもいいなって感じました。
これによって、既定の動作を選択するための仕組みが、自己責任なコード側から、安全・確実なコンパイラ側に移るというところに感動しました。
抽象クラス問題
また、石川さんのスライド p27 から始まる、抽象クラスの継承問題のところも興味深かったです。
そう、Swift には抽象クラスという考え方がないので、fatalError を使うくらいしか、それが抽象メソッドであることを確実に伝えられないんですよね。しかも、親の実装を呼んではいけないという配慮が必要だったり、不適切でも実行時エラーまで気づけなかったり、何かと不便が多いところです。
これについては、石川さんのおっしゃるのとおり Protocol で万事解決できそうな印象です。抽象クラスが欲しいという意見もあるようですけど、自分的には今のところ、プロトコルがその代わりを完全に担ってくれそうかなって感じました。
また、石川さんのスライド p29 の「抽象化による型の損失」も面白かったです。
このコードを見たとき『あれ、そうだっけ』と思ったんですけど、確かめてみたらおっしゃる通りでした。引数の型が既定クラスから派生クラスに変わったときってオーバーライドにならないんですね。
たしかに、これは大きな損失です。
プロトコルの Self が求めるもの
ちなみにプロトコルの Self
というキーワードについて、これをクラスでも頑張れば使うことはできるのですけど、それでも今回の損失を埋めることはできないはずです。
というのも、この Self
というキーワードは『自分自身の型』を表すものなのですけど、これをクラスの、メソッドの戻り値やプロパティで使ったときに限っては、未来も含めた派生先の型を保証してくれるんですけど、メソッドの引数として使ったときには『そのプロトコルを準拠させた型』を意味することになります。
そのため、石川さんの例であれば Orderedクラス
で準拠したプロトコルの Self
の場合、それが想定する型は Ordered
そのものになり、今回の石川さんの例と同じ損失が起こり得ます。その上でその派生先の Numberクラス
で同じプロトコルに準拠させると、今度は Number
を想定したものを新たに実装することが求められます。
そんな感じで、クラスにプロトコルを使うと特に Self
周りで混乱しがちな印象なので、そういう面からも、データ型をクラスで実現するというのはあまり向かないのかなって思えるところでした。
ちなみに、メソッドの戻り値やプロパティで使う Self
の場合は、未来も含めた型を返すことを求められます。
これについては @akio0911さん さんが良い例を挙げてくださいました。
SelfとdynamicType #swift2symposium pic.twitter.com/xGAsznJfjZ
— あきお@大阪アプリ開発講座8/1 (@akio0911) 2015, 6月 28
こんな風にして、未来を見越した dynamicType を駆使する必要が出てきます。
もちろんクラスが final
であれば未来はもはやここまでなので、dynamicType
を使わずに自分自身を素直に返して大丈夫です。
ところで required init
ってこういうときに使うんですね。自分はこれを知らなくて、プロトコルを使って init
の実装を求めるようにしてたんですけど、そのときに required
を付けろとコンパイラが言う意味がやっと理解できました。
これ以降の未来では「必ずこのイニシャライザが存在している」ことを保証するためのもの、と思って良さそうですね。これは良いこと教えて頂きました。
クラスの final 指定について
会の中で沸き起こった議題で興味が湧いたもののひとつに、クラスに final
をつけるか否か、というのがありました。
自分は、クラスを適切にオーバーライドできるかどうかはクラス設計に依存すると思っていたのもあって、継承されることを想定していないクラスをオーバーライドできることって、あまり良いことではないのかなって思います。
そんな理由で、継承クラスが作られることを想定していないクラスには必ず final
をつけるのがいいのかなって思いました。Swift の場合は、クラス継承よりもプロトコルを使った組み上げ方が主体のようにも見えるので、むしろ明記しなければ継承できないくらいでも良かったのかなとも思います。
これについて、クラス継承が不可欠という意見も挙がってきました。
その理由が「テストに不可欠だから」という視点で、それを自分は考えたことがなくて興味深かったです。派生クラスにはそういう応用の仕方もあるんですね。
そしてそうなると、構造体のテストについてはどうなるんだろうという疑問も湧きます。クラスと構造体はまったく違う意味合いが違うものでもあるので、構造体で派生したテストができなくても支障がないかもしれませんけど、そうだとすれば、もしかしてテストのために final を使用しないという方法の代わりに、クラスのテスト方法を再考する余地があってもいいのかな、とも感じました。
非同期処理での Error Handling について
もうひとつ、話題に上がった非同期処理での Error Handling の活用についても興味深い話が伺えました。
戻り値をエラーに変換するというアプローチに好印象
特に興味深かったのは、稲見さんのスライド p48 からの、非同期処理における Error Handling のアプローチとして、実行時に throw するのではなく、戻り値に throw するというアプローチでした。
前提として非同期処理を見据えて、その中で Error Handling をどのように活かして行くことができるのか、そんな点に着目して深く掘り下げて行く様子と、そのアイデアのひとつとして挙げられた「戻り値をエラーに変換する」という方法が綴られています。
着眼点を変え、エラーを投げるタイミングを遅らせることで、非同期処理でもエラーハンドリングを実現するところが、とても面白かったです。
主眼はどうやってコールバックにエラーの重みを持ち込むか?
そんな興味深さはひとまず横に置いて、Error Handling そのものに視点を向けたときには、自分はやっぱり、その魅力って『エラー処理を強制できる』ところにあるように思えます。
そんな観点で話を聞いていたとき、自分の中の関心はそれに向き過ぎていて、たとえば「戻り値をエラーに変換する」というアプローチのところで『変換しないという選択も採れてしまうのではないか』というところが気になってしまってました。
その観点から、Error Handling は同期的なエラーに対する解として、そして非同期に対する解はこれまでの Result (Either) を使う方法が適切かな、という感想に、当時は辿りついていました。
ただ、それから公開資料で復習しながらゆっくり思いを馳せていたところ、話題の根底のひとつに「Either だと正常もエラーも重みに区別がない」こと、つまり「Error Handling をコールバックにどう上手く持ち込むか」がひとつの争点として掲げられているのに気がつきました。
稲見さんのスライド p57 あたりの話ですが、自分は話を聞きながらこの辺りをはっきり意識できてなかったので、振り返れば、的を得た意見が言えてなかったように感じました。
そのときの自分の意見は、たぶん「どうやってエラー判定を強制させるか」と「Error Handling はそもそもスレッドを跨ぐことを考えるものではない」というのに意識が囚われていて、稲見さんの発表に即した投げ掛けにはなっていなかったかもしれません。
読み返すほどに面白い
しかしこうして改めてスライドを読み返してみると、読むほどに稲見さんがスライドの中で解決しようとしている課題が素晴らしく広いことに気づいて、これは聞く以上に読み応えのあるスライドだなーってただただ感心させられました。
今にこうして「もしかしてこうするといいかな?」とか「ああするといいかな?」みたいなことをあれこれ思い描いてみるも、真新しい案を考えたつもりが結局は稲見さんのスライドのどこかに合流するという面白さ。
何度も何度もスライドに納得させられて、何故だかお得な心地がしました。
rethrows が印象的でした
そんな Error Handling の話題については @cockscombさん
も話されていて、そこで出てきた rethrows
の話が特に興味を引きました。
この rethrows
については、加藤さんのスライド p23 に特に興味が湧いて、発表を聞きながら「なんか面白い使い方ができそうだな」と思いながらも理解しきれなかったんですけど、終わってから振り返ってみると、なかなか興味深い特徴があることに改めて気づかされました。
たとえば throws
が付いた関数であれば、その中で do-catch なく try
だけ書けば、呼び出し元にそのまま throw
されるので、なんとなくリスロー?みたいなことはできるんですけど、それと実際の rethrows
とは、肝心なところが大きく違う様子でした。
整理すると、どちらとも try
を catch
なしで使える仕組みながら、次の違いがあるようでした。
関数につける語 | 想定されるエラー |
---|---|
throws | 関数中で catch されなかったエラーを呼び出し元に伝える。新しいエラーを伝えることも可能。 |
rethrows | 引数で渡されたエラーを想定した関数で発生したエラーに限り、呼び出し元に伝える。 |
大きな特徴としては rethrows
をつけた関数では、その内部で新しくエラーを発生させることはできないようです。できることは、引数で受け取ったクロージャーが発生させたエラーだけを、そのまま発生させられること、つまりエラーのリダイレクトみたいな感じでしょうか。
つまり、呼び出し元が想定したエラー以外は発生しないため、呼び出し元が、発生し得るエラーを確実に想定できる安心感があるように思います。
そして感心したのが、その特徴が言語仕様でもしっかり活かされていて、もし引数にエラーを想定しない(throws
が付けられていない)クロージャーが渡されたとき、rethrows
でエラーが発生しないことが明確なので、呼び出しを try
で装飾する必要がなくなるというところでした。
// rethrows を使った機能の場合、
func test(closure:() throws -> String) rethrows -> String {
return try closure()
}
let p:() throws -> String = {
return "throws"
}
let q:() -> String = {
return "no throws"
}
// エラーが発生するクロージャーに限り try が必要に
try test(p)
// エラーが発生しないクロージャーでは try が不要に
test(q)
こんな性質から、たとえば単純に「引数で受け取ったクロージャーが発生させるエラーをそれ全体のエラーとして再送出したい」ときって throws
でも出来なくないですけれど、それよりも絶対に rethrows
を使った方が、意図的にも正しいし、安全性も高まりそうです。
そしてこれの大きな副産物として「エラーを想定しないクロージャーを受け取ることもでき、その場合は Error Handling が不要になる」という、コードの可読性や合理性が高まるところも魅力に感じました。
rethrows の取り柄を活かせるところ
そんな rethrows
について、加藤さん自身が次のようにおっしゃってました。
そういえば言い忘れたけど map とかに rethrows 付けておいてほしいのだった #swift2symposium
— Hiroki Kato (@cockscomb) 2015, 6月 28
なるほど、たしかにこういう map
みたいな、任意のクロージャーを受けてそれを主体に処理をするみたいな機能に対しては rethrows
が付いていると、勝手が良いかもしれません。
たとえば、配列全体に対する計算でエラーを想定したい場面があったときに、現行の map
では Error Handling を活かすことができないようです。
// たとえばエラーを想定したクロージャーがあったとき、
let calculate = { (v:Int) throws -> Int in
return v * 100
}
// rethrows のないマップだと Error Handling を活かせない
let answers = try arr.map(calculate)
これがもし、次のように rethrows
をつけた map
が用意されていたとしたら、ずいぶん様子が変わってきます。
extension CollectionType {
func map2<T>(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T] {
var result = [T]()
for element in self {
try result.append(transform(element))
}
return result
}
}
仮にこのようになっていたとすると、先ほどとまったく同じコードでスマートに Error Handling を活かすことができる、ということになる感じのようでした。
// たとえばエラーを想定したクロージャーがあったとき、
let calculate = { (v:Int) throws -> Int in
return v * 100
}
// rethrows のあるマップだと Error Handling を活かせる
let answers = try arr.map2(calculate)
そして、このときさらに嬉しいのが、この関数にエラーの発生しない(throws
のつかない)クロージャーを渡したときには、map
を呼び出すときに try
の要らない、従前とまったく同じ書き方でコードを書けるという点です。
つまり、本当にエラー処理が必要なときだけ Error Handling を記載できるということになる、ということですね。これはたしかに map
に rethrows
がついていてくれると、嬉しいことになるかもしれません。
非同期時のコールバック
そして、加藤さんのスライド p25 の非同期におけるエラーの扱いの考察も興味深いところでした。
こうしてブログを書きながら復習していてようやく気がついたのですけど、発表の時間軸的にはその後の稲見さんの内容と重なるというか、別のアプローチから見た別表現的な形で、同じ本質を捉えた発表、だったのですね。
発表当時は自分の理解が追いついてなくて、その後の稲見さんに、この加藤さんの p27 と p28 のことを質問してましたけど、こちらに既にちゃんと書いてありました。
そう、非同期で Error Handling を試みるときに、f を取る方法も、Result に get() を備える方法も、もしかしていいかも?って思ってしまうんですけど、突き詰めていくと最終的な決定打があと一歩、欠けてしまっているんですよね。
今回の稲見さんの話と加藤さんの話のおかげで、自分の中で掘り下げきれてなかった部分がかなり明確に整理できた気がします。そうしてみたとき、加藤さんが例に挙げた p31 の Unreal 例がなかなか面白く見えてきますね。どことなく気持ち悪い気もするコードですけど。
Swift をどうやって勉強するか
そして話を最初の発表に遡らせて、@sonson_twitさん の「Swift をどうやって勉強するか」というテーマも面白かったです。
今回の勉強会の導入にふさわしい内容だなーと思って、この発表を最初に配置した岸川さんの組み立ての上手さが光ります。
Ruby でコンパイルできる C 言語
そんな中でもひときわ興味深かったのが「Ruby でコンパイルできる C 言語」という表現でした。
とても見事な表現で、特に Swift の場合は Objective-C の流儀のまま Swift を書いている限りはまったく何もメリットがない、というところにとても納得しました。
自分もそれに近いところはそういえば感じたことがあって、たとえば相互運用性のおかげで Swift で書いたコードを Objective-C でも普通に使えるわけですけれど、Swift ならではの記載は大抵使えないため、それを意識したコードを書くと、結局まどろっこしいコードになってしまうんですよね。
それよりは、Swift 純粋なコードを書いて Objective-C で使いたいときはブリッジクラスをひとつ追加で作った方がよっぽど楽にコードが書けるなと思うこの頃でした。
Swift の方が楽にコードが書けるという利点も、きっとちゃんと Swift として書くからなのでしょう。逆に Swift って書いていてもなんかごちゃごちゃするばかりで・・・と思う人は、もしかするといったん Objective-C のことは忘れて Swift に正面から向き合ってみると、新しい楽しさが見えてきたりするかもしれません。
自分の Swift 勉強方法
この発表の反応を見ていて、きっと誰しもがそうだったと思うのですけど、どうやって Swift を学んでいったらいいか、そのとっかかりが分からないという意見が多かったように思います。
そういえば、自分はどうやって Swift のとっかかりを掴んだかなって思い返す良い機会なので、ちょっとそんなところを振り返ってみたくなりました。
最初は愛想を尽かしてみたり
自分はそもそも WWDC 2014 で Swift が登場した瞬間に、これは楽しそう!と思ったのが始まりでした。
Objective-C とはずいぶん違う印象で、最初の印象は簡単そう、そしてどことなく自分が好きな C++ と似ている印象があって、実際にコードを書いてみると面白くてすっかり夢中になったものでした。
しかしある日に登場した Beta 2 での仕様変更に、唖然としたのを覚えてます。
このときに衝撃的だったのが、範囲を表現する演算子がたしか .. から ..< に変更されて「Swift はまだこんな程度の品質なのか・・・!」と、早くもここでいったん愛想を尽かせてしまいました。
その後、とある事情で Swift を勉強しないといけない状況に迫られて「どうしてそういう仕様になっているの?」「どうしてそんな風に変わったの?」と Swift に問いかける癖がついてから、Swift に対する印象が再び好転したのでした。
Swift の気持ちになると見えてくるもの
やっぱり、真新しい、何にも縛られない言語だからか、よくできているように思えるんですよね。
自分は関数型プログラミングとか難しいところはまだ理解がほとんど進んでいないのですけど、それでもひとまず Swift のひとつひとつの言語仕様を、Swift 全体から俯瞰してみたとき、なるほどそれでそういう風になっているんだなっていう発見があって、それが自然と、いわゆる Swift らしさへと導いていってくれるように感じました。
たとえば、
- let と var って単純に定数と変数の区別だけなのか
- 値をオプショナルで表現するということはどういう意味を含むのだろう
- 列挙型が網羅性を大事にするのは何を期待しているからだろうか
- 列挙型で列挙しが(既定では)数字で表現されなくなった理由はなぜだろうか
- なぜこんなにも switch 文に力が入れられているのか
- なぜこんなにも列挙型に力が入れられているのか
- 構造体とクラスとがなぜあそこまで似通った構文で存在しているのか
- それでいて mutating とか deinit とか微妙なところで差が生まれてくるのはなぜなのか
- Swift 標準ライブラリに、クラスがなぜほとんど存在しないのか
とか。
これらの問いかけに自分なりの答えが見つかってくると、だんだんと「Swift って、構造体とプロトコルで世界が回っているのかな」っていう感じがしてきます。
そして、自分の中の想像だけかもわかりませんけど、構造体の存在意義が見え始めると、だんだんプロトコルが主体になっていることが見えてきて、プロトコルを中心に組み立てたいと思えてきて、そうなるとそもそも Swift のクラスってなんのために在るものなのか、Objective-C のクラスとは意味合いがそもそも違うのではないか、そんな風にだんだん話が広がってきたりして。
そんな風にして、とりあえず今に至ってます。
そんな流れで学んだ Swift だったので、何から始めたらいいか迷って足が止まったのなら、初めて言語を学んだときみたいな気持ちで Swift に向かってみるという方法をとっても、近道ではないでしょうけど、次第に Swift の世界観へと導かれるんじゃないかなって思いました。
ただ、これは Objective-C ではこうだったから、みたいな見方をすると足をすくわれがちにも思えました。たとえば構造体がぜんぜん違うものであるように、クラスだってそもそもぜんぜん想像と違うものかもしれない、そんなくらいの気持ちで臨んでみるほど、得られるものは多いのかなって印象でした。
この頃、気になる flatMap
そんな自分が、この頃に見えてきた次の目標に「なるべく!は使わない」というのがあります。
吉田さんのスライド p9 に書かれている心がけの 1 つ目なのですけど、自分はとりあえず今のところは、けっこう「!」を好んで使っています。理由は「コードが冗長ではなくなるから」がいちばん近いと思います。
たとえば、配列に絶対に入れたはずの要素を使ってインデックス番号を取得するコードを記載するとき、エラー判定をわざわざ書くのは冗長、と自分は思います。
array.append(element)
if let index = array.indexOf(element) {
}
else {
}
こうしてしまうと、絶対にある値に対してエラー判定が書かれていて、読む側がそのコードをぱっと見たときに、どうしたときに else ブロックが処理されるのかを読まないといけなくなってしまいますし、その可能性をついつい考慮したくなります。
また、コードを書く側にとっても、else のときに何を書くかを僅かながらに迷わなくてはいけません。将来、間違ったコード修正でもしない限り、ぜったいに実行されないコードであるにもかかわらずです。
それならいっそ、次のように書いた方が「ありえない!」感が主張できていて読みやすいように感じます。
array.append(element)
let index = array.indexOf(element)!
もしもなんらかの事情で element
が存在しなかったとしても「!」で落ちてくれるのがオプショナルの醍醐味です。
ただし「落ちるのは実行時」というのが大事なところで、実行時に落ちるよりはビルド時にエラーになったほうが、動作の観点で見た安全性も高まりますし、コード修正のコストも抑えられて良いことは間違いないでしょう。
余談ですが、石川さんのスライド p27 にもあった、抽象クラスの継承問題をプロトコルで解消しようというアプローチも、この「実行時エラー」を嫌った例のひとつです。
そんな「!」による実行時エラーの発生を回避する手法の一つとして map
や flatMap
を使った方法がある、そんなお話を最近、ようやく自分は耳にしました。
これについて、自分はまだぜんぜん特徴や使いどころを掴めていなくて、でも想像するに、きっとそもそもの流れから随分違ってきそうな予感がします。実行時エラーをビルド時エラーに変換するのではなく、そもそも実行時エラーを無に変換する、きっとそんなアプローチになるのでしょう。
今後はこんな辺りも意識してコードを書いてみたいなって思うこの頃です。そうしたらまた新しい Swift の気持ちに出逢えそうで楽しみです。
まとめ
こんな感じで、たっぷりと Swift の深みに触れて学びを深められる、とっても楽しい勉強会でした。
議論が前提になっていたこと、議論の起こす目的のパネリスト参加枠が用意されていたこと、参加者が大勢いたこと、ツイッターで情報共有できたこと、ツイッターでの質問をすぐさま拾ってみんなで議論されたこと、Playground ですぐさま検証できたこと、談義し合う時間が十分に確保されていたこと、会場が和やかだったこと、発表順番が臨機応変だったこと。
いろんなことが織り成して、すごく楽しい、有意義な会が生まれたのかなって感じました。
参加されていたみんなの満足度も高かったみたいで、帰り際、たっぷりと嬉しそうにしながら帰る人がいらして、見ていてそれがとても微笑ましかったです。そんなことが、何よりいちばん最後に心に残る、とても嬉しい勉強会でした。
素敵な会をありがとうございました。