今回からは、これまでに見てきた 強参照循環
を打開するための手段として在る 弱参照
と 無所有参照
のそれぞれの特徴についてを確認していく回になる見込みです。ただその前に Twitter で話題に挙がっていた lazy
まわりに少し興味が湧いてしまって、まずは寄り道、そこから眺めてみようと思っています。よろしくお願いしますね。
———————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #226
00:00 開始 00:23 今回の展望 00:43 かつての NSObject と unowned が絡んだバグ 04:17 lazy プロパティーの活用場面 05:49 ほかの lazy プロパティーが効果的な場面は? 07:28 lazy プロパティーの使用が向かない場面 10:16 無限に続くシーケンスを相手に扱うことも 10:55 lazy が標準動作だったとすると? 12:49 遅延評価可能なコレクション 17:05 どちらが良いかを考える 18:19 制御フローの違い 20:06 都度評価される可能性に注意 24:46 lazy の所感と次回の展望 ————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #226
では、始めていきます。
今日は勉強会の内容が少しネットワーク的な話題に寄ることがあるので、それを見ていこうと思います。前回は「Unowned」について話しましたが、今回は Swift と Unowned の細かい特徴に加え、特定のバグについても触れます。
前回の勉強会で提示された「Unowned のバグ」の話についてまず紹介します。このバグは2018年1月に報告されたものです。内容を簡単に説明すると、「NS オブジェクトを継承したクラスを unowned
プロパティとして保持するとクラッシュする」というものでした。具体的には、プリントした時にクラッシュする問題が報告されています。
このバグは当初の問題解決までには時間がかかったものの、最終的には原因が比較的シンプルなものだと判明したようです。実際に Playground で試してみると、このバグが再現するかもしれません。ただ、現時点(少なくとも macOS の環境では)では問題なく動作しているようです。
このバグは初期段階で発見され修正されているため、ユーザー側で特に何かをする必要はなく、気にかける必要もあまりなさそうです。Unowned でクラスを保持するというパターンは少ないかもしれませんが、気になる方はぜひ試してみてください。
次に、全く違う話題になりますが、Twitter で見かけた「Lazy プロパティ」についてです。Lazy プロパティを皆さんカジュアルに使っているでしょうか?私はあまり使っていなかったのですが、この便利さを改めて実感しました。
例えば、以下のようなコードがあります。
class Example {
lazy var expensiveProperty: Int = {
return expensiveComputation()
}()
}
このようにすることで、初めてプロパティーにアクセスした時にのみ計算が実行され、無駄な計算を避けることができます。特に、初期化時の処理が重い場合には有効です。
実際に、この Lazy の効果を見てみると、驚くほど処理が早くなります。これを知った時は、自分のコードレビューに役立てたいと思いました。主に初期化の際に処理を重く感じていた部分が軽減されるのは非常に有益です。
Lazy プロパティが役立つ場面は、例えば初期化時に時間のかかる処理やリソースを多く消費する処理を遅延させる場合などです。ただし、全ての場面で有効というわけではないので、適材適所で使い分けることが重要です。
この勉強会の内容を通して、様々な環境や状況での最適なプログラミング手法を学んでいきましょう。 「確実に」と言っていいのか分からないけど、ファーストは早くなりますよね。レイジでラストは全然関係ないよね。レイジでラストは取れないのかな、分かんないけど。でも、とにかく素晴らしい。これ、ちょっと今度から使おうと思います。適当に使うときもあれば、やばいときもあるでしょうけどね。グラフィックスを取るときも使えるのかな?使えますね、きっと。グラフィックスもすごいです。
自分が使うといい感じです。こんな感じでいろいろとリプライが賑わっていたような気がするので、その辺も見てみますかね。せっかくだからね。これも何だろう?こっちかな、こっち。これが元になってるんですね。こうなって、使うべきか使わないべきか考えずにとりあえず入れる感じ。ここですよね。せっかくならいろいろと見ていきたい感じです。適切に使っていければよいですね。
レイジを使わないほうがよい具体的な例として思いつくのは、レイジを使っても結局は例に変換されてしまったり、その道筋の要素が最後まで評価される必要があるときですね。その二つどっちも、これは最後に書いたレッテを強化する必要があるときの例です。このときは確かにそうですね。途中でグラフィックスとか使うようなときは、もちろん全部は消化してないですけど、元のやつね。そういったときには効いてきそうですね。だったら全てレイジをつけとけって話になるわけではなさそうです。詳しいところは分かんないですけど、Haskellみたいな言語は遅延評価が標準っぽいですね。あれは遅延評価をする代わりにオーバーヘッドというのかな、体積が大きくなって余計重たくなってしまう問題もあるらしいですね。
今回のツイートがたまたま速度が注目されているから速度の話が多いですが、速度を改善するためだけにやみくもにレイジを使うと、時々足をすくわれることがあるそうです。速度だけじゃないですからね。無限に続くシーケンスの中からうまくファーストを取るとか、そういったときにも役立つんです。無限に続くシーケンスからいろいろマップを使って、その中からプレフィックスを取るようなときにも役立つし、速度以外の観点でもちょっと広く見たほうがいいですね。
他にどんな返信がついているのか見てみると、「ディフォルトをレイジにして、逆にそうじゃない場合にする」という発想があって、これは面白いですね。そこでHaskellの話が出たんですね。なるほど、どっちのアプローチも確かにありですね。言語次第でランダムアクセスができなくなったり、いろいろな性質が消えたりするのが問題としてありますね。要は、レイジはこういった世界観で作られていないだけで、もしレイジが標準になったら、世界が変わるかもしれないですね。
コレクションがかなり強化されたプラス面にはなりますが、その分オーバーヘッドも増えてくるということですね。このレイジシーケンスがどんな機能を持っているか、ちょっと見てみるのも面白そうです。後で見てみましょう。レイジシーケンスにするのも面白いですね。 Swiftの言語仕様についていろいろ調べていくと楽しいですよね。特に、言語使用的な対応も面白いと思います。レイジシーケンスを用いる場合、シーケンシャルな処理が必要になります。ランダムアクセスコレクションは、ランダムアクセスを保証しており、これに適合しているため、ランダムにいろんなところの値を取ることができます。
ランダムアクセスコレクションというのは、どの値を取ろうとしてもオーダー1で取れるものです。ここで言われているランダムアクセスは、オーダーというよりもインデックスアクセスのことを指しているようです。インデックスアクセスであれば、ランダムアクセスコレクションまで行かなくても、通常のコレクションで保証されます。この点を明確にするのが難しいですね。
レイジシーケンスでもインデックスアクセスをするためにサブスクリプトを使用することができますが、計算量や現実時間での実行時間を考慮しなければならない点があります。それを除けば、他の手法によっても作り次第で対応可能です。
レイジシーケンスでコレクションを使用する場合は、レイジコレクションと呼ばれることもあります。これがコレクションで使われるとレイジコレクションとなる、という話です。具体的な場所や用法は分かりませんが、レイジコレクションの応用により、計算量を除けば普通にコレクションのように動作させることも可能です。
レイジランダムアクセスコレクションが発動するとどうなるかについては、オーダー1で飛べれば良いのですが、そのオーダー1がどの程度の計算時間になるかは別の話です。このようにレイジを活用して様々な設計を考えるのも面白いですね。
引用されているツイートがとても良いらしいので、まずその流れを見てから内容を確認したいと思います。難しいことが分からない場合、両方試して速い方を選ぶ方法もあります。ただし、今回は速度について注目されていますが、全体的にはどちらが良いかを試してみるのが大事です。速さだけでなく、文脈やコードによって良さが変わることもありますので、両方試して観察する発想は重要です。
4分でループする方法も一つの選択肢です。4分で回すのとレイジを使う方法は基本的に対等です。この中で流れを変えるためにエラーハンドリングを使うと、意味合いが異なってきます。
要するに、プログラム設計においては、単純な力技だけではなく、多方面からの考察と試行が重要であり、それによって知識が増えるということですね。 選んだからといって、そういった点も考慮して実行してみれば面白そうですね。さっき褒められていた理論も、部分的に評価しない可能性があるときには有効ですが、何度も評価される場合にはコストになることがあります。そうか、なるほど。当たり前ですが、内部でキャッシュを取ることで変わることもあります。遅延評価が主体の言語では、そのキャッシュのことをなんと言うんでしたっけ?実行しながらキャッシュを取る方法があり、それをすることで次回以降が早くなり、全体のアルゴリズムを高速化する手法があるのを忘れてしまいましたが、それも重要です。
たとえば let values = [1, 2, 8, 8, 2, 3, 4]
などのようにして、それを関数内で function sum(values: [Int]) -> Int { return values.reduce(0, +) }
のようにヒントを取って実行すると、遅延評価の関数がすぐに計算されてしまうことがあります。つまり、二回評価されることがあり、毎回計算されることがコストになります。
例えば values[0]
を何回も実行した場合、遅延評価があると、その回数分評価されます。これを回避するためにキャッシュを使うことができます。 values.prefix(2)
のようにプレフィックスを取ることで、同じ問題が発生することがあります。この場合は、無限に続くシーケンスなどから要素をいくつかだけ取るためにキャッシュを使用することでパフォーマンスが向上し、全体の計算負荷を減らすことができます。
このように、遅延評価を適切に使うためには特定の配慮が必要であり、そのために最後に値をキャッシュしておくことが重要です。高機能を標準にすることで言語のパフォーマンスが上がる反面、初心者には難しくなることがあります。そのため、適切な場所で遅延評価を使うことが推奨されます。
今回は時間になってしまったので、遅延シーケンスの詳細な機能については次回以降の勉強会で触れることにしましょう。ウィークとアンフォールドの実際の特徴についても、次回以降で見ていこうと思います。
今日はこれで終わりにしましょう。お疲れ様でした。ありがとうございました。