https://www.youtube.com/watch?v=NFjAcrK25RE
今回は Swift.org から About Swift (https://www.swift.org/about/) の Swift の追加機能のお話。そこの範囲やコレクションに対する高速で簡潔な反復処理から引き続き眺めていこうと思います。どうぞよろしくお願いいたしますね。
——————————————————————————— 熊谷さんのやさしい Swift 勉強会 #5
00:00 開始 01:23 高速で簡潔な反復処理 03:22 for ⋯ in 06:29 in の後に書けるもの 09:15 for ⋯ in で使える型を作る 20:05 Sequence と Collection 24:56 NSFastEnumeration 26:48 構造体の高機能化 27:57 構造体が振る舞いを持つ 32:03 クラスと構造体の類似性 34:20 プロトコル指向 40:46 関数型プログラミングパターン 42:29 map, filter, reduce 46:30 map 47:46 filter 48:35 reduce 49:13 メソッドチェーン 50:18 高階関数 51:04 次元を減らす 54:55 flatMap 57:40 クロージング ———————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #5
では早速進めていきましょう。前回は関数ポインターとクロージャ、タプル型について、あとジェネリクスをぱぱっとやった感じなので、その続きからやっていこうかなと思います。
前回の感想として、タプル型が面白かったですね。話を聞いていると、タプル型を結構活用されている印象がありました。自分はあんまり使っていないなと思っていましたが、意外と知らず知らずに使っていたのだなという発見がありました。それ以上に意識的に活用されている印象があって、とても面白かったです。もしタプル型に興味がある方がいましたら、前回のアーカイブを見てもらえればいろいろと見つかるものがあるかなと思います。
今回は、範囲やコレクションに対する高速で簡潔な反復処理がSwiftから新たに追加されたという話を進めていきましょう。いつでも話しかけてくれて、話の流れを切ってくれても大丈夫なので、気軽に参加してください。
それでは、早速プレイグラウンドでやってみましょう。範囲やコレクション、要はSwiftでの配列ですね。配列と聞くとイメージがつかみやすい人が多いかなと思います。用途が複数持てるものがコレクションと言われています。
まずは、ループが簡単に書けるというところから見ていきます。Swiftを始めた人はもうお馴染みかと思いますが、昔のCやObjective-Cを使っていた人にとっては、随分と配列の扱いが楽になったという印象があるかと思います。例えば、for value in values
と書くだけでループが回せるというのがSwiftの新機能の一つですよね。
以前は、以下のように書いていましたね。
for (int i = 0; i < values.count; i++) {
value = values[i];
}
こんな感じで一生懸命書いていた時代もありました。
Swiftではこういう書き方をしなくても、for value in values
というシンプルな書き方ができるようになりました。実質的にはIteratorを使っているわけですが、これは次のようなイメージです。
var iterator = values.makeIterator()
while let value = iterator.next() {
// ループ処理
}
このようなことを裏で自動的にやってくれていると考えれば分かりやすいかもしれません。
Objective-Cの時には、インデックスを使った書き方をしていましたが、Swiftの場合は何も難しいことを考えずに、このfor in
構文で回せるようになったのは便利ですよね。
そして、このfor value in values
のin
の次に書けるものとしては、コレクションやイテレータがあります。ちょっと試してみましょうか。例えば以下のようなものです。
for value in values {
// ループ処理
}
このvalues
のところに書けるものは、コレクションプロトコルに準拠したものというルールがあります。そのあたりも考慮すると、より表現力の高いコードが書けますね。
では、イテレータも試してみます。 まずは、コレクションに準拠したものではないと使えないという決まりがあります。
他に興味あることがあったら、全然チャットでも声でも割り込んでもらえれば大丈夫です。
さて、このvalues
に使えるものは、コレクションに準拠している必要があります。例えば、以下のようなデータ型のインスタンスを使いたい場合でも、このままでは使えません。
struct Data {
// データ構造
}
let data = Data()
シーケンスに準拠させると使えるようになりますね。 そうそう、シーケンスから始めましょう。コレクションは要素が複数あるものを扱えます。コレクションと範囲は少し違いますが、一般的にはコレクションと呼ぶことが多いです。
例えば、for-in
文でこれを使いたいとします。例えば、ランダムな値を返す型を作りたい時に、for
ループで乱数を扱いたいとしましょう。これでデータが分かりにくいかもしれませんが、こういう感じで作ってみることにしましょう。例えば、関数 value
を計算型プロパティーとして定義し、このプロパティーを呼び出すと Int
型の乱数が手に入るようにします。return Int(arc4random())
のように書くことで、ランダムな値を返す処理を作ることができます。
このように乱数を生成する関数を作りましたが、これだけだとまだ for
ループで回す方法が分かりにくいですね。愚直に書いてみましょう。例えば、変数に乱数を持たせて、for
ループでそれを取り出すというやり方があります。
var randomValues: [Int] = []
for _ in 1...100 {
randomValues.append(Int(arc4random()))
}
for value in randomValues {
print(value)
}
このように書くと、乱数が100個生成されて、それを順番に出力することができます。ただし、これでは少し冗長です。
次は、シーケンスを使ってもう少しスマートに書いてみましょう。値を順番に取り出したい時は、シーケンスを使うことで効率的に処理が行えます。
まず、RandomValues
という型を定義し、これをシーケンスに準拠させます。シーケンスに準拠させるためには、makeIterator
メソッドを定義する必要があります。
struct RandomValues: Sequence {
func makeIterator() -> some IteratorProtocol {
return AnyIterator {
return Int(arc4random())
}
}
}
こうすると、この RandomValues
型を for
ループで扱うことができるようになります。
for value in RandomValues().prefix(100) {
print(value)
}
これで、ランダムな値が100回出力されるようになります。ここでは、prefix(100)
を使うことで、ループが100回に制限されています。
Swiftのシーケンスとコレクションを使えば、このように簡潔に反復処理を記述することができます。プレイグラウンドが反応しない時もありますが、再起動して解決することが多いです。この勉強会でも、再起動を多くすることになると思います。 いろんなことをこういう機会で紹介しているので、わからないことがあったら興味のあることを一つだけ見つけて持ち帰るような気持ちでいてもらえれば、今後の役に立つかと思います。わからないことが多くて頭がパニックになりそうな場合でも、一つだけ拾って持ち帰るといいかもしれません。
この勉強会の一番の特徴はAppleの公式ドキュメントをじっくり読む機会が多いことです。知らない事柄が出てきた際に調べてみると、Appleのドキュメントは非常に良くまとまっており、じっくり読んでいくと様々な発見があり、とても面白いです。もし質問や突っ込みがあれば、遠慮せずに言ってください。その方がより多くのことを話せるので嬉しいです。
これまでの内容で十分かとは思いますが、簡潔な反復処理についても触れておきましょう。前回、シーケンスについて話しましたが、シーケンスはイテレーターを作ってそのイテレーターで順に回していくというものです。ランダムアクセスまではいかず、インデックスを使って取り出したい場合には不向きです。しかし、配列などではインデックスを使って要素を取り出すのが便利です。このような場合にはシーケンスではなくコレクションというプロトコルを使うことになります。
では、コレクションがどのようなものか見てみましょう。具体的には、Array
の定義を見ると、Arrayがコレクションであることがわかります。ランダムアクセスコレクションというのはインデックスを使ってどの要素にも同じ計算速度でアクセスできるものです。これを見ると、ランダムアクセスコレクションはBidirectionalCollection
を継承しています。
BidirectionalCollection
とは、先頭インデックスから順にアクセス(0, 1, 2, 3, 4, 5...)することも、逆に末尾インデックスから前に戻っていく(...5, 4, 3, 2, 1, 0)の両方ができるコレクションです。そして、BidirectionalCollection
は通常のコレクションも継承しています。
通常のコレクションとは要素を内包し、インデックスでアクセス可能なもので、最初のインデックスやシーケンスのように先頭から順に取っていく以外にも、最後の要素の次を表すエンドインデックスまでが決まっています。一般的なコレクションはイテレーターも取れますし、シーケンスとしても振る舞えます。
つまり、コレクションもシーケンスの一種なので、for-in
文のところで使えるわけです。非常にざっくりとした紹介になりましたが、このようなイメージです。もし捉えにくい部分があれば、質問してください。
こんな感じでSwiftの基礎を学んでいきます。ちなみに、余談ですが... えっと、余談になるのが、配列やコレクションに対する高速で簡潔な反復処理っていう話です。さっきのスライドにも書いてありましたね。その「高速な反復処理」っていうと、思い出すのがObjective-Cの時代の話です。Objective-Cには「NSFastEnumeration」というものがありました。これをよく使っていた方や、思い出深い方もいるかと思います。
Swiftの新機能で「高速な反復処理」と言っているのは、多分これと同じようなことです。Objective-Cで高速な反復処理を書くときに「NSFastEnumeration」プロトコルに準拠させて、for...in
構文を使っていましたね。確かに、その時もfor...in
という構文を使えたと思いますが、それと同じような感じですね。あまり詳しい説明をここでする必要があるかはわかりませんが、Objective-Cで「NSFastEnumeration」を使いたくなったら、この話を思い出してください。これが、配列やコレクションに対する高速で簡潔な反復処理の話です。この分野の詳細については、今後また勉強会で取り上げることがあると思うので、そのときに詳しく見ていきましょう。
次に、構造体によるメソッド実装や型拡張、プロトコル準拠のサポートについて話しましょう。これがSwiftが出た時に非常に注目された特徴の一つです。今までC言語の構造体では、データ型を定義再定義するものでした。既存のデータ型を複数まとめて1つの新しいデータ型を再定義するために構造体を使っていましたが、それ以上のことは基本的にはなかったわけです。
しかし、Swiftでは構造体にメソッドの実装や型拡張、プロトコル準拠までできるようになりました。これは非常に大きな特徴です。ただ、この話をする際にプロトコル準拠をサポートしている点を捉えれば十分かなと思います。
具体例を見てみましょう。たとえば、Int128
という型を作りたい時の話です。以下のようにストラクトを定義します:
struct Int128 {
var high: Int64
var low: Int64
}
これはC言語の構造体の機能だけを使って定義したものです。しかしSwiftではさらに、メソッドやプロトコルを追加することができます。例えば、次のように掛け算のメソッドを追加することもできます:
struct Int128 {
var high: Int64
var low: Int64
func multiplied(by value: Int128) -> Int128 {
// 実際には計算ロジックをここに書きます
// とりあえず仮の値を返します
return self
}
}
さらに便利にするために、以下のようなスタティックメソッドを用意することもできます:
struct Int128 {
var high: Int64
var low: Int64
static func *(lhs: Int128, rhs: Int128) -> Int128 {
// 掛け算のロジックをここで実装します
return lhs
}
}
このように、型自身が振る舞いを持つことができるのはSwiftの大きな特徴です。そのため、多くの人が「クラスと構造体の違いは何だろう?」と悩んだことがあるわけです。クラスはもともとメソッドを持てますが、構造体もこのようにメソッドを持つことができるので、その違いについて考える必要が出てくるわけです。このあたりの詳細は、セルフの話をするときに補足しようと思います。 とりあえず、self
を使っていなかったら、これでちゃんと綺麗にコンパイルが通ります。クラスならもともとプロパティもメソッドも持てますし、そうすると参照型と値型の違いぐらいかなとか、いろいろそこからイメージが広がっていきますが、要は今まで構造体は機能を持てませんでした。でもクラスは機能を持てるので、それで使い分けていた人にとっては非常に分かりにくい状況になったりもしましたね。
これはまた構造体の話の時にいろいろ見ていこうかなと思いますが、とにかくこうやって機能が似てきてしまった場合、やりたいことというのは要はプロトコル思考が絡んでくるのではないかと思うんです。クラスと構造体って非常に使い分けが難しくて、実際にどんな視点でそれらを見るかによっても、その使い分けは多分変わってくると思います。
なので、どれが正解とかいうのではなく、このアプローチの時にはこちらかな、別のアプローチの時にはこちらかなという感じになっていくと思います。まず状況をしっかり想定して、その状況の中でクラスと構造体の特徴を比べていくと、その使い分けが見えてくる、自分なりの答えが見えてくるのではないかと思います。そういう意味で、構造体とクラスに向き合ってみると面白いかもしれません。
とりあえず、このプロトコル思考というアプローチで見たときに、構造体がメソッドを実装できるというのがとても大事な特徴になってきます。例えば、これでね、print
、そのA
をプリントしてみますか。
とりあえず print(A)
とやったときに、通常はここまで出ちゃうか。構造体だとこういう風に原始的な表現をしますが、もっと適切な表現にカスタマイズしたい場合があります。例えばこの場合、行が A.row
で範囲が A.range
のようにね。本当は128ビットとして適切な数字を表示したいのですが、この時間内にちゃんと自分が書ける気がしないので省略しますが、こうしてあげるとちゃんと値が出ます。上位の値が何なのかみたいな表示ができます。
同様に、B
も表示させたいという場合、昔の考え方だと同じように書いてあげて、B
も表示されるようにするわけです。しかし、そのインスタンスが適切な内容を表示できるものであるときには、CustomStringConvertible
というプロトコルを準拠させてあげると、カスタマイズできます。これがプロトコル志向的な考え方です。
そこで、Int128
をCustomStringConvertible
に準拠させてあげ、そのプロトコルが必要とするdescription
を定義します。その中でさっきの表示方法を組み込んであげると、わざわざ毎回128ビットの数字を表示させるときに同じコードを書かなくても良くなります。単にprint(A)
と書くだけで標準的な表示方法をしてくれるのです。print(B)
も同様です。
こうすると、わざわざ両方とも同じコードを書かなくても、標準の出力をしてくれる振る舞いができます。CustomStringConvertible
に準拠したインスタンスは、そのインスタンスそのものを表現するテキスト表現に変換できるという、プロトコル志向的な考え方があるわけです。
こうやったときに、非常にすごいと思うと同時に、構造体であっても型拡張ができてプロトコルに準拠でき、メソッド(今回は計算型プロパティを付けましたが)が実装できるのは、とても大事なポイントです。これらができないとプロトコルに準拠できず、プロトコル志向が行えないとなると、Swiftのコンセプトが揺らいでしまいます。ですので、必然的に構造体もこれらの機能をサポートする必要があったと捉えることもできます。
この機能があることによって、構造体にもプロトコル志向の考え方を持っていけるようになり、今までオブジェクト指向ではクラスでしかできなかったことが構造体でもできるようになります。これが非常に画期的であり、重要なポイントです。
とにかく、メソッド実装や型拡張、プロトコル準拠が可能になることで、Swift全体の可能性が広がる大事な新機能と言えます。 それでは、次にマップとフィルタのような関数型プログラミングパターンについて見ていきましょう。これは Swift についての話です。
関数型プログラミングパターンという言葉が登場します。実際に、Swift が登場して間もない頃は、関数型プログラミングが大好きな人たちが「Swift は関数型プログラミング言語なんじゃないか」と熱狂していました。その姿が印象的でしたが、少し落ち着いてきました。個人的には関数型プログラミングはおまけ的な感じかなという印象を持っています。
要は何かというと、メソッドチェーンのことです。関数型プログラミングという言葉を聞いてイメージしにくい人は「メソッドチェーン」をイメージしてもらえれば良いと思います。特に Objective-C をやっていた人だとイメージがしやすいかと思います。
では、実際に関数型プログラミングと言われて出てくるものと言えば、代表的なメソッドとして forEach
、filter
、map
、そして reduce
が挙げられます。forEach
はおまけ的なものですが、map
、filter
、reduce
が重要です。
まず map
ですが、これはコレクションやシーケンスの各要素に対して関数を実行し、その結果で新しい配列を作るものです。次に filter
ですが、これはシーケンスまたはコレクションの各要素から条件に一致するものだけを取り出して新しい配列を作り直します。そして reduce
ですが、これは複数の要素から1つの結果を導き出すというものです。
例えば、配列の各要素を半分にして新しい配列を作る場合、以下のように map
を使います。
let values = [2, 4, 6, 8]
let halvedValues = values.map { $0 / 2 }
print(halvedValues) // [1, 2, 3, 4]
次に、filter
を使って 5 より大きい値を取り出す場合です。
let values = [1, 5, 10, 5]
let filteredValues = values.filter { $0 > 5 }
print(filteredValues) // [10]
reduce
を使って合計を求める場合は以下のようになります。
let values = [1, 5, 10, 5]
let sum = values.reduce(0) { $0 + $1 }
print(sum) // 21
さらに、これらの関数を組み合わせることもできます。例えば、配列の各要素を半分にし、それを 3 より大きいものだけにフィルターし、最終的に合計を求める場合、以下のように書けます。
let values = [2, 4, 6, 8]
let result = values.map { $0 / 2 }
.filter { $0 > 3 }
.reduce(0) { $0 + $1 }
print(result) // 0, because there are no elements >3 after map
このように、関数を組み合わせて結果を導くのが Swift の大きな特徴の一つです。関数型プログラミングのパターンとして紹介されることが多い map
、filter
、reduce
は非常に便利で強力なツールです。今後もこれらを使いこなせるようになると良いですね。 クロージャーや関数を使えるようになると、この辺りがスムーズに進み、面白い表現ができるようになります。それが大きなポイントの一つです。
さて、リデュースの話に移ります。リデュースというか次元を減らすという話を見ていこうかと思います。コメントもなかなか興味深いですね。リデュース自体は要素をまとめるようなところがあります。機械学習でも次元削減をリダクションと呼びますが、確信をついています。
例えば、次元を減らすとどうなるか考えてみましょう。これを次元を減らすと見ることができますね。これは1次元の配列ですね。1次元配列を価値観として見ると、これはInt
型なので0次元にしている感じですかね。他にも配列は複数次元にすることができます。2次元配列や3次元配列を0次元や1次元にすることもできますし、逆に次元を増やすこともできます。
例えば、ここで2次元配列を考えてみましょう。2次元配列はこういう感じで書きます。配列に渡ってきた値を入れ込んで、合計するというものです。動くはずですが、動かない場合は以下のようにしてみましょう。
let result = array.reduce(0) { $0 + $1 }
これで動きますね。これをプリントしてみると、次のようになります。
print(result)
これで次元が減ります。純粋に次元を減らすという意味ではflatMap
も同じような処理です。フラットマップとマップの例もそのまま使えます。例えば、次のようにします。
let flattenedArray = array.flatMap { $0 }
print(flattenedArray)
Reduceは次元を変える機能でもありますが、そこが混乱の原因かもしれません。Reduceでできることは多く、非常にパワフルです。配列に適用することでいろんなことができる便利な機能です。
それでは、時間になりましたので今日はここまでにします。皆様、お疲れ様でした。