引き続き、お気に入りな技術ブログ「その Swift コード、こう書き換えてみないか」で紹介されている事例を見ていきますね。今回は for
ループの二重ループに着目して product(_:_:)
を使うことを提案されているところから眺めていきます。慣れてくるほどに二重ループは必要とあれば普通に持ち出せるものと思いますけれど、そこに対して「考える」隙間を投げかけてくれるこの問いも絶妙でいいなと思ったりしながら、自分にとって馴染みのないところでもあるのでゆっくりと読み進めていけたらいいなと思っています。よろしくお願いしますね。
————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #262
00:00 開始 00:27 Swift Algorithms の product 関数 01:52 PRODUCT とは 03:12 Excel における PRODUCT 関数 04:32 Swift における product 関数 06:12 配列に対する積 07:17 product 関数の利点 08:55 product 関数の実装 12:08 片側がシーケンスで、片側がコレクション 13:46 product 関数についての印象整理 14:18 見る深度によっても印象は変わる 17:34 product 関数を使ってみる 18:30 product 関数の利用場面 20:12 手続きではなく意味で捉えられる 21:21 product 関数が発想できないこともあるかも 22:48 計算速度は気にするほどではなさそう 23:48 zip とは異なる挙動 24:42 年数を範囲で表現してみる 28:07 コアライブラリーではないのが悩ましい 28:40 for ⋯ in と forEach の使い分け 32:06 for ⋯ in と forEach における特色の違い 34:08 forEach より for ⋯ in の方が使うかもしれない 35:02 クロージングと次回の展望 —————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #262
はい、始めていきましょう。今日は引き続き、個人的にとても気に入っているSwiftコードの書き方について話していきます。これは、あるブログ記事を元に進めています。
今回の話題は「プロダクト」という関数についてです。まず、プロダクト関数とは何かというところから見ていきましょう。どうやらこの関数はSwift Algorithmsの中に含まれているようです。このブログを書いている方はSwift Algorithmsと親しいようで、頻繁に使っているライブラリだと伺えます。一般の開発者だとプロダクト関数の存在を知らないこともあると思いますし、特に目的がない限りSwift Algorithmsを導入しないこともあるでしょう。
プロダクト関数は興味深い関数です。簡単に言うと「計算積」を求める関数です。たとえば、サム(Sum)は和を求めるのに対し、プロダクト(Product)は積を求めます。ただし、プロダクトにはいくつかの意味があり、この文脈では2つのシーケンスを掛け合わせる、いわば行列の掛け算のようなものを指しています。
Excelにも似たようなプロダクト関数が存在します。実際に使ってみて、その便利さを確認してみましょう。
まず、Excelのプロダクト関数についてです。=PRODUCT(a1, a2)
という形で使い、これでa1とa2の積算を行います。複数のセルを指定すると、それらをすべて掛け合わせて積を求めることができます。試しに、利益率と販売数量を掛け合わせて利益額を計算する場合、=PRODUCT(b2:b8, a2)
といった感じでサクッと計算結果が出ます。とても便利な関数です。
ただし、Swiftのプロダクト関数はこれとは少し異なります。他のプログラミング言語でも似たようなプロダクト関数が存在しますが、例えばRuby言語の場合 Array#product
というメソッドがあります。このメソッドはレシーバーの配列と引数で与えられた配列の要素をそれぞれ掛け合わせて、新たな配列を作成します。すべての組み合わせを網羅するわけです。
これを使うと、例として配列 [1, 2]
と [3, 4]
を掛け合わせた場合、結果は [[1, 3], [1, 4], [2, 3], [2, 4]]
となります。すべての組み合わせができる、非常に便利な関数と言えるでしょう。ということで、次はSwiftでの使用例を見ていきます。 「配列だと思ったけど、タプルっぽくとられればSwiftではわかりやすいかもしれません。例えば [1, 4, 15, 24, 25, 34, 35]
みたいに、配列で取得できるときにこういう感じでシーケンスになっているのかなと思います。これからゆっくり見ていきますが、行列や配列に対する駅というのはこういうもののことを指します。Swiftのアルゴリズムの中にこういったものがあるのですね。
これを使うと、例えば年と月で各年の各月をループするときに便利です。確かに、手動で年を回して月を回して何年何月と全部網羅する必要がありません。Swiftのアルゴリズムを使うことで、product(years, months)
とすることで簡単に全て取得できます。これによってネストを一段階減らすことができる利点があります。また、product
に渡したシーケンスの片方が空だった場合にも無駄なループ処理が発生しません。
面白いですね。例えば、'months'が空の場合、'years'があってもループは回らないためプリント文が実行されないという話です。正規化すれば無駄なループは無くなるかもしれませんが、それも状況によりますね。
次に、このproduct
関数で無駄なループが発生しないということが重要です。この状況になると、メソッド自体は残りますが、無駄なループは削除されるかも知れません。プロダクト関数を呼ぶときにシーケンスを引数として渡すので、内部で何か処理をしてループが回らなくなる場合、そのアドバンテージはどうでしょうか。
例えば、自分で書いたループが回らなかった場合でも、プロダクト関数を使った方が最適化に無視されない可能性もあります。プロダクトの実装を見ると、ソースコードではプロダクトがどのように動いているかが分かります。
ソースコードのアルゴリズム部分に product
や product2Sequences
があり、それを使うことでベースとなるシーケンスとコレクションを扱えます。例えば、シーケンスで受け取った場合、キャッシュができないため、保証が難しくなることがあります。それを回避するためにイテレーターを使って処理を行っています。
具体的には、二つのイテレーターを持ってそれを順に回していきます。この辺りは zip
シーケンスに似ていますね。プロダクト2シーケンスがこのような処理を行っているため、最適化が削除される心配も少ないです。
コメントを見ると、ベース2のシーケンスの場合、ベース1のイテレーターが保証できないからこのようにしているようです。ベース1が無限シーケンスであった場合、ずっと回ってしまう可能性があるため、それを防ぐための措置かもしれません。
こういうアプローチは面白いです。他にも、両方がシーケンスではダメなのか、といった設計の問いもありますが、実際に試してみないと分からないところもありますね。」 イテレーターを持っていると最適化の面で不利な気がします。このような重たいものを持たせると問題が生じるかもしれませんが、まぁいいでしょう。
プロダクト関数を呼び出すと渡されたベースシーケンスのコレクションにイテレーターを作成し、そのプロダクトシーケンスにそのまま渡します。そこから呼び出されるままにイテレーターが処理され、ベースを返していくという作りになっています。
プロダクトは無駄なループ処理が発生しないように設計されていますが、フォーループも見る視点によっては同じ処理をどのレイヤーで見るかによって異なります。そのためどのレイヤーで無駄なループ処理が発生しないのかを考えながら理解を深めるのがおすすめです。
抽象化と具象化についても、自分の立っている位置によって見え方が変わることがあります。ある立場からは具象化に見えるが、別の立場からは抽象化に見えることもあるのです。このような相対的な視点の違いを意識すると、異なる見方や理解が生まれて有益です。また、他人の言っていることがその人にとっては正しいこともありますし、間違っていることもあるので、それも含めて理解するのが重要です。
この視点から見れば、このプロダクト関数で渡したシーケンスが nil
を返す限り、フォーループに投入されることはなく、無駄なループは発生しません。まさにこの説明通りです。こういった箇所を最適化しようとするのも大事ですが、どこを追求するかで見方が変わるので、それも面白いところです。
そろそろプロダクト関数の具体的な話に移ろうと思います。ここまでの話で、だいたいのイメージがついているかと思いますので、実際に動かしてみます。
まず、import Algorithms
して、オードペット(コード)を貼って、フォーループで回しました。当然ながらアルゴリズムスは最初から使えるようにしなければなりませんね。プレイグラウンドで実行してみたところ、うまく動作しました。結構かっこいいですね。 タイトルが少し誇張されている部分もありますが、二重のフォールループを直接使うのではなく、適切な場合にプロダクト(product)を使うという話です。具体的には、二重のフォールループで何かの組み合わせを作る際に、プロダクトを使うと効果的だということです。例えば、for x in xs
とその中で for y in ys
という形でループを回して組み合わせを網羅する場合が、まさしくこの例です。
このようなときには、プロダクトを使うことを知っておくと便利です。例えば、イヤーとマンスのプロダクトを使ってループを回すと、コードが分かりやすくなります。以下のように書けます。
for (year, month) in product(years, months) {
// 処理を行う
}
このように、プロダクト関数を使用すると、ソリフトアルゴリズムズが使える環境ではコードが見た目も分かりやすくなります。最近では、説明的なコードスタイルに偏っていると思いますが、個人的には手続きを重視することも良いと思います。
例えば、以下のように手続き的なコードを書いた場合:
for year in years {
for month in months {
// 処理を行う
}
}
これだと具体的にループが回っている様子がわかりますが、何をしているのかが少し見えにくくなります。一方で、プロダクトを使った場合は説明的で、コード全体が近代的で分かりやすいです。
確かに、プロダクト関数の存在を知らないと、直接ループを書いてしまいがちです。しかし、プロダクトという関数があるなら、それを使わない手はありません。これを知らないために読みやすいコードを書けないのはもったいないです。このような便利な関数を積極的に活用すると、コードの可読性が向上します。
Swift Algorithmsというライブラリを使うのも一つの方法で、標準ライブラリとして取り入れると良い選択肢になると思います。この勉強会の内容をきっかけに、Swift Algorithmsの素晴らしさを再認識し、利用するのも悪くないと思います。 素晴らしいですね。きれいにまとめられています。平算速度とかあまり気にしないことが多いですが、このブログを見ると少し気になってきます。それでも、あまり重要ではないかもしれません。さっきのコードを見た限り、特に重さは感じませんでした。どちらも、ほぼ同じようなことをしているようです。
次に進む前に何か質問があれば、どうぞという感じですね。二重ループを使う場合、簡単に zip
を使うと処理速度が落ちたりしますが、その点について特に言うことはありません。また、ループを二重で使う場合、外側のループと内側のループを組み合わせる形になりますが、これは普通にコードを書けば解決されることです。例えば、
for i in 0..<10 {
for j in 0..<10 {
// 何かの処理
}
}
という感じですね。間違いではありません。その他としては、配列のループに関しては、少し工夫する余地があるかもしれません。
質問にどんどん答えていきますが、例えば範囲のサイズについて気になることがあります。範囲は Range
型で、多分 upper
と lower
が設定されていると思います。まず型を確認しましょう。タイプは普通の ClosedRange
ですね。これを確認するために、例えば以下のようにしてみます。
let range = 1...10
print(type(of: range)) // ClosedRange<Int>
ClosedRange
は構造体になっています。lowerBound
と upperBound
のように設定され、具体的なサイズはメモリとして確認できます。このようにすることで、16バイトになる場合があります。これを Int8
に変えると、4バイトに減少します。このようなメモリ管理の観点も考慮しないといけません。
また、範囲をうまく表現するときには、その範囲に注意を払う必要があります。「1月から12月」というように表現すると範囲内の操作が速くなります。具体的には以下のようにします。
let months = 1...12
for month in months {
// 何かの処理
}
このように範囲をうまく利用することで、効率的なコードを書くことができます。
次に、for-in
ループと forEach
の使い分けについて考えてみましょう。これは非常に良いテーマですね。特に Swift
が登場したばかりのころによく議論されていた内容です。for
と forEach
だけでなく、言語が提供する標準的な機能とカスタムした機能の違いに注目すると、その使い分けが見えてきます。
重要な点としては、スコープの作成方法や break
や continue
、return
などの制御構文がループの種類によってどう影響されるかです。特に return
と throws
について考える必要があります。
たとえば、for-in
ループでは以下のように書けます。
for item in array {
if item.condition {
continue
}
// 何かの処理
}
一方、forEach
では以下のようになります。
array.forEach { item in
guard !item.condition else { return }
// 何かの処理
}
このように、制御構文がスコープに与える影響を考慮しながら、状況に応じて適切なループを選択することが大切です。以上が for-in
ループと forEach
ループの使い分けの概要です。 今回はSwiftの勉強会で、「for-in」と「forEach」の違いについて説明しました。
基本的に「for-in」と「forEach」は同じように使えることが多いですが、重要な違いがあります。たとえば、「for-in」ではループを途中で終了するために break
を使うことができますが、「forEach」ではそれができません。特殊な処理をしたい場合やエラーハンドリングをする場合には、「forEach」向けにフラグを用意するなどの工夫が必要です。
さらに、次の要素に移りたいときには、「for-in」では continue
を使うことができますが、「forEach」では基本的にはそのような使い方はできません。ただし、リターンを使ってループ全体から抜け出すことはあります。
また、同期処理や非同期処理の観点でも違いがあります。「for-in」では非同期処理について柔軟に対応できますが、「forEach」ではそれが難しい場合が多いです。これはAPI次第である部分もありますが、「forEach」はあくまでメソッドチェーンとして使うことが多いので、メソッドチェーンに含まれない処理には適していない場合があります。
コメントや質問に応じて、今回は「for-in」をメインで使うべきケースと「forEach」を使うべきケースの具体例をもう少し深掘りして話しました。最後に、もう一度内容をまとめて、次回もう少し詳細に見ていくことにしました。
今回はここまでで終わりにします。お疲れ様でした。ありがとうございました。