今回は、これまでに見てきたオプショナルバインディング
の最後の項目、複数の条件を組み合わせてそれを使える話を眺めていきます。難しいものではないですけれど、もしかすると思いのほか出番の少ない機能かもしれないので、この機会に少しゆっくり眺めてみましょう。それを見終えたら引き続き、自動強制アンラップ周りの機能についておさらいしていく予定です。どうぞよろしくお願いしますね。
今回はゆめみ社外に向けた公募はなかったようなので、社内メンバーでの開催になります。
———————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #167
00:00 開始 01:03 前回のおさらい 03:01 reduce を再起呼出で再現してみる 04:35 いろいろコードを書き換えてみる 05:27 省略表記を明記してみる 06:11 クロージャーを関数型の変数で扱う 07:53 演算子を関数として扱う 09:08 正の符号演算子 09:32 加算演算子 09:48 Xcode の癖のある挙動 10:06 加算演算子の定義箇所を探す 10:37 加算演算子も関数として渡せる 12:30 reduce(0, +) は一般的? 13:31 ここでジェネリクスを使うことにした理由 16:58 型パラメーターの命名について 17:43 Sequence から最初の要素を取るには? 20:30 型パラメーターの特化 26:51 関連型を特化させた不透明型の扱い 27:32 プロトコル拡張でも特化を利用可能 28:51 特化したプロトコルに対するプロトコル拡張 30:02 特化と型エイリアスを用いたプロトコル拡張の新しい表現 32:30 ジェネリックプログラミング時代の到来 35:47 クロージングと次回の展望 ————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #167
では、始めていきましょう。今日も引き続きオプショナルについてお話しします。ただ、先へ進む前に先日目に留まった前回のプレイグラウンドについて補足したいと思います。前回参加していなかった方もわかるように、内容を振り返りながらお話ししていきますので、前回いなかった方はそれを聞きながら復習として活用してください。
まず前置きとして、前回はオプショナルな配列についてお話ししましたが、今回は普通の配列についてお話しします。具体的には、配列の reduce
メソッドについてです。reduce
メソッドは初期値を与えて、順番に配列の要素を処理するための高階関数です。前回は reduce
と reduce(into:)
についての実装や、その違いについて比較しました。Appleの実際の実装を確認したところ、表向きのAPIの使い方の違いが主な差であり、実質的な違いはほとんどないという結論に至りました。ただ、使う側の頭の使い方に影響する点があるため、そこを考慮することが必要です。
前回の勉強会では、時間の都合もあり、途中で中断したコードがありました。それは reduce
を使わないケースのコードです。それを今日完成させて、皆さんに紹介しようと思います。以前も似たような内容で reduce
を使わずに書く方法を紹介しましたが、今回は再帰関数を使って書く方法をお見せします。
具体的には、配列の要素を一つずつ取り出して、それを除いた残りを再帰的に calculate
関数で処理する方法です。要素がなければ何も影響しません。この方法は再帰的なアプローチでゼロや単位元(影響を与えない値)をうまく扱うものです。この形で実行すると、reduce
を使用した場合と同じ結果になります。
では、早速コードをいじりながら説明していきましょう。まず、再帰関数を使ったコードを見て、それから reduce
を使ったコードに書き換えてみます。具体的な例として、次のようなコードを考えます。
func sum(_ array: [Int]) -> Int {
guard let first = array.first else {
return 0
}
return first + sum(Array(array.dropFirst()))
}
このコードは、再帰的に配列の要素を足し合わせていくものです。
一方、reduce
を使った場合は次のようになります。
let array = [1, 2, 3, 4, 5]
let sum = array.reduce(0, +)
どちらの方法も結果は同じになります。では、これを応用しながらいろいろと書き換えていきますので、もし良いアイデアや改善点があれば、ぜひ共有してくださいね。
このあと続けてコードの具体的な書き換えを行いながら、reduce
メソッドの活用方などについて議論を深めていきます。よろしくお願いします。 これは、いろいろなことを明記した説明になります。書き換えの話ですが、これはプログラムの品質を向上させるために非常に重要です。よくありがちなのは、二つの整数を取り、整数を返す関数を使う場合です。パラメーターとしてクロージャーを取るのが一般的です。ラベルなしで取る場合、例えばJavaScriptをやっている人には、次のような書き方がイメージしやすいかもしれません。
例えば、クロージャー adding
を定義し、整数を二つ受け取ってそれを足し合わせるクロージャーとして次のように書くことができます。
let adding = { (currentValue: Int, nextValue: Int) -> Int in
return currentValue + nextValue
}
このように、関数ではなく名前付きのクロージャーとして書くことができるわけです。
また、Swiftの演算子はすべて関数として定義されているという特徴があります。つまり、足し算演算子(+)も関数として定義されているのです。例えば、次のような定義があります。
static func +(lhs: Int, rhs: Int) -> Int
このように、スタティック関数として定義されるため、名前空間に属しているグローバル関数のように使うことができます。演算子は名前空間を指定せずに使えるので、関数型として扱うことができる嬉しい特性があります。
次に、演算子を丸括弧で囲むと、関数型として扱うことができます。例えば、次のように足し算演算子を代入して使うことができます。
let addFunction: (Int, Int) -> Int = (+)
そして、このように代入した後に使うこともできますが、直接関数として使うことも可能です。例えば、次のように書くことができます。
let result = addFunction(3, 5)
print(result) // 8
まとめると、Swiftでは演算子を関数として扱うことができ、パラメーターとして関数型に直接渡すことができる便利な特徴を持っています。演算子を丸括弧で括ることで関数型として扱えるため、コードの柔軟性が高まります。これがSwiftの一つの大きな魅力です。 なので、Reduce
でいきなり演算子を渡されても、その事情が分かると他の場面でも演算子を渡せるようになります。そうすると、コードを書く側としては面白くて、良い書き方ができると思います。もちろん、どの語彙を使うかはチームによって異なるので、チームでコードを書く場合は慎重に行った方が良いでしょう。Reduce
はチームでも使うことが多いでしょうね。私自身、あまりチームでコードを書いた経験がないので、実際どうなのかは分かりませんが。
Reduce
を使って引き合わせていく書き方が分かると便利です。名前の付け方も工夫すると良いですね。関数のエンドランスの特定理などについて話し始めると脱線しそうなので、他の機会にすることにします。
今回の話では、Reduce
の使い方についてコレクションを使っています。その理由について少し見ていきたいと思います。なぜここでコレクションを使っているのかというと、Generics(ジェネリクス)を使用しているためです。例えば、DropFirst
の定義は純粋にArrayスライスですね。ここで配列をイントの配列にすると、条件の都合でDropFirst
が最終的に渡せなくなります。しかし、ジェネリクスを利用することで、Arrayスライスだろうと他の型だろうと受け入れられるようになります。
Array型に変換する必要がない理由は、Arrayのためのメモリ領域がヒープ領域に確保されるなど、メモリ管理が煩雑になるからです。また、これがジェネリックプログラミングによる利点の一つです。ジェネリクスを使うことでパフォーマンスのオーバーヘッドを回避できます。
例えば、E
という要素をコレクションに受け取る場合、あまり普段使わない名前ですが、使い勝手の良い名前にします。今回は数字を減算するので、Int
型にします。そして、エレメント型としてwhere文で制約をかけ、Int
として受け取るようにします。
APIデザインガイドラインでは、名前は型の制約ではなく用途で決めるとされています。この例では少し曖昧ですが、ここまででとりあえず動くところは確認できました。最初の要素さえ取れていれば、シーケンスとして扱えるので良いでしょう。 シーケンスの最初の要素をどうやって取るかについて、少し混乱してしまいました。シーケンス
の定義を辿っていくと、最初の要素を取るためのメソッドがいくつかありますが、現時点では確認できないようです。例えば、シーケンス
にはドロップファースト
などのメソッドがあります。ただし、ファースト
そのものは違うメソッドのようです。
次に、コレクション
について話を進めます。例えば、コレクションに対していろいろと操作を行ってみたいと思いますが、バイナリ検索に変えてみるのも面白いですね。ただ、今回は基本的なジェネリクスの話に限定して進めます。特に、Swift 5でのジェネリクスの進化についてはあまり詳しい説明はできませんが、最近追加されたサム
というキーワードについて触れてみます。
例えば、あるコレクションをパラメータに指定せずに、サムコレクション
型のようにして書けるようになりました。しかし、この際にエレメントを整数型に固定するのが難しいんです。また、ウェア条件を付けることもできません。例えば「サムコレクションのエレメントが整数型
」としたい場合、それが実現できません。これは、通常のサムコレクション
を取ると、そのエレメントが何なのか明確でないからです。
この問題を解決しようとすると、独自のプロトコルを使用することが一つの方法です。ここでマイコレクション
というプロトコルを作り、これがコレクション型に準拠するようにすれば、その範囲内でコンパイルが通るようになります。さらに、エクステンション
を使用して、配列や配列スライスをマイコレクション
に準拠させることもできます。
以下はそのコード例です:
protocol MyCollection: Collection {}
extension Array: MyCollection {}
extension ArraySlice: MyCollection {}
このように書くと、配列や配列スライスに対してマイコレクション
として振る舞わせることができます。ここにエレメントの型情報が必要なら、次のような形で書くことができます:
func functionThatAcceptsMyCollection<T: MyCollection>(_ collection: T) where T.Element == Int {
// ここに処理を書く
}
こう書くことで、ジェネリックな型Tが必ず整数エレメントを持つコレクションであることを保証できます。この方法を使えば、コレクションのエレメント型に関する柔軟性を保ちつつ、特定の型に限定した操作も可能となります。
次のステップとして、サブシーケンスを少し整理し、なるべく扱いやすくするために、コレクションのアソシエイティブタイプに準拠させることを考えます:
protocol MyCollection: Collection where SubSequence: MyCollection {}
このようにして、コレクションのアソシエイティブタイプを整えることで、さらなるエラーチェックが通るようになります。今後、これらの方法を試していくことで、より理解を深めていきたいと思います。 今後、自分でプロトコルを作成するときには、パラメーターとしてどのような型を使用するかをあらかじめ記述しておくと非常に応用が効くようになります。これを怠ると、コードの12行目でエラーが発生し、「スペシャライズできないよ」というメッセージが表示されます。これは、パラメーターが指定されていないためです。
例えば、タムという型で受け取るときに、Tというパラメーターを使いたい場合は、プロトコルにその情報を書いておくのが良い方法です。従来のオールドスタイルの書き方の場合は、T
としてコレクションを受け取り、その条件について記述することが一般的です。これによって、標準的なプロトコルが整備されていけば、コードの可読性が向上するでしょう。
今後、Swift 5.1に向けてプロジェクトを開発する場合、この種のパラメーターをプロトコルにどんどん追加することが重要になります。これにより、プロパティに対しても特定の型を指定することができるようになります。この方法がスペシャライズにおいて非常に興味深いものと感じました。
さらに、スペシャライズの書き方が多岐にわたって導入可能になったことで、例えば、エクステンションの際にMyCollection where Element: BinaryFloatingPoint
のように、特定の条件を満たす要素に対して関数を実装することが可能です。これにより、特定の型を満たす場合にのみ機能を拡張することができるようになりました。
例えば、配列に対してArray where Element == Double
というエクステンションの書き方をすることが可能となり、これにより配列の要素がダブル型の場合に特定の機能を追加することができます。これは非常に革新的で、タイプエイリアスを使った拡張も可能となり、コードの可読性やメンテナンス性が向上するでしょう。
ただし、タイプエイリアスでのエクステンションを行う際には注意が必要です。誤解を招く可能性があるため、基準をしっかりと設定した上で使用することが重要です。それにもかかわらず、型エイリアスでエクステンションができることは非常に理解しやすく、有用な方法だと思います。 この書き方は、未来があるかもしれませんし、「ちょっとやめておこう」となるかもしれません。この転換点や変化は非常に興味深いので、ぜひ試してみて、レビューに投げて反応を見てみてはいかがでしょうか。
IKEAを呼び出すと全然関係ないのですが、ジェネリクスに関連したのでご紹介しました。そんなところですかね。もともとお話していたように、もっと別の書き方ができるというお話もありますので、それについてもぜひご意見をお聞かせください。
これはたまたま見つけたのですが、うっかりコードを書いてしまいました。タイプエリアとは違って、メタ的に使ったことのない話でした。これまで、Swift 2の時からジェネリクスは浸透し始めていましたが、既存のプログラミングスタイルにジェネリックプログラミングが便利機能として追加されているだけでした。しかし、今回のAny
の搭載により、ジェネリクスがもっと頻繁に使われるようになると思います。1、2年後には当たり前に使われるでしょう。
この書き方が「typealias MyCollection<Int>
」という形でwhere
を省略するものです。これにより、複数の値を取る場合でも明快な表現が可能になります。以前はwhere
を使って、例えば「typealias T
のエレメントはInt
、typealias U
のエレメントはString
」のように書く必要がありましたが、これが非常に分かりにくかったです。この新しい書き方を好んで使う人が増えていくと思いますので、今のうちに慣れておくと良いでしょう。
こういった標準的な書き方、つまり「常識」を作っていくのは大事です。個人でも影響を与えることができ、スタンダードを作っていくのは面白いです。難しいことではなく、1年もあれば自分の考えたやり方が浸透していくこともあります。勉強会で新しい書き方を紹介し、それを発信することで、影響力を高めることができます。
今日は次回やることも紹介して終わりにしようかと思います。次回は「複数のオプショナルバインディングと条件判定の併用」について見ていきます。この部分も基本的な構文なので、知っている人は普通に使っていると思いますが、改めて見てみると新たな発見があるかもしれません。
時間になりましたので、今日はこれで終わりにしましょう。お疲れ様でした。ありがとうございました。