はじめに
swiftはかなり楽しい言語です。私はとても気に入っています。クロージャにも慣れました。ですが、どうにも不慣れな関数群があります。高階関数とか呼ばれるあれ、例えば、map
です。
人様のコードをみていると、私だったらfor
文で書くところを、map
を使ってさらっと一行で書いていたりしていて、すごくかっこいい。単純に憧れます。
私も使えるようになりたい。
というわけで、理解するために自分用に噛み砕いたメモを作ってみます。(swift5で動作確認しながら作成)
map
簡単な例として、あるタプルの配列から一部だけを抜き出した配列を作成する事を考えます。
素直にfor
文で書くとこうです。
let list = [("Tokyo", 22), ("London", 18)]
var cityList:[String] = []
for (city, _) in list {
cityList.append(city)
}
// cityList == ["Tokyo", "London"]
これが、map
を使うとこうなります。
let list = [("Tokyo", 22), ("London", 18)]
let cityList = list.map { $0.0 } // ["Tokyo", "London"]
おお。かっこいい。スッキリしています。
ですが、私には、cityList
がString
の配列になることが直感的に見えてきません。よくわからないけど、コメントを信じて次へ進むことしかできません。
理解するために、なぜこうなるのかを紐解いてみます
map
の定義
上述のmap
関数は、Array
構造体のメソッドです。定義は以下の通り。
(実際にはthrows
やrethrows
などのエラー制御用のキーワードがありますがここでは省略します)
func map<T>(_ transform: (Element) -> T) -> [T]
この定義から、map
メソッドは、(Element) -> T
型の関数(クロージャ)をひとつ受け取って、T
の配列を返すメソッドであるとわかりました。結構シンプルな形をしています。
関数の説明によれば、このクロージャはマッピングクロージャと呼ばれています。(数学ではmapは写像という意味のようです)
なお、T
は任意の型を意味します。(ご存知ない方は後述のジェネリクスを参照してください。)実際に使うときに、型を決めて使います。Tの部分がみな同じ型になる、という点だけ押さえればよいです。
Element
は元の配列の中身であって、型はIntの配列ならInt、Stringの配列ならStringになります。今回の例はタプルの配列なので、Element
はタプルです。
配列のmap
メソッドは、その配列の項目を一つずつ、このマッピングクロージャに渡して実行します。クロージャの戻り値を順に配列に入れて、最終的にその戻り値の配列を、map
メソッドの結果として返してきます。つまりマッピングメソッドの出力を配列にしてくれます。
なんだか'map'が単純な関数に思えてきました。今回はタプルの配列から文字列部分だけを抜き出したいので、マッピングメソッドで受け取ったタプルの文字列部分を返すようにすれば、文字列の配列が得られることになります。目的にぴったりな便利関数です。
この理解から、最初のfor
ループの処理をmap
に置き換えてみます。
let list = [("Tokyo", 22), ("London", 18)]
let cityList = list.map({ (city: String, _: Int) -> String in
return city
}) // ["Tokyo", "London"]
クロージャで配列の要素をうけとって、その中の欲しい情報を戻り値にすれば、その配列が出来上がる、というわけです。なるほど、簡単です。
しかし、見た目は別にわかりやすくないし、シンプルでもありません。かっこよくもない。これだったら、まだfor
文の方が、わかりやすくて断然マシだと思えます。
ここから、swiftの仕様を使って、強引にシンプルな見た目に近づけていきます。
クロージャの定義を引数のカッコの後ろに出す
swiftでは、関数の最後の引数がクロージャの場合、引数定義の()の後ろに出して書くことができる、というルールがあります。Tailing closure と呼ばれる書き方で、UIView.animate
などでおなじみ(?)です。
// これを
UIView.animate(withDuration: 0.3, animation: {
self.aView.alpha = 1
})
// こう書いてもよい。
UIView.animate(withDucation: 0.3) {
self.aView.alpha = 1
}
これを使って書き直します。
let list = [("Tokyo", 22), ("London", 18)]
let cityList = list.map() { (city: String, _: Int) -> String in
return city
} // ["Tokyo", "London"]
そして、map
の次の()
も(他の引数がないので)省略できてしまいます。
let list = [("Tokyo", 22), ("London", 18)]
let cityList = list.map { (city: String, _: Int) -> String in
return city
} // ["Tokyo", "London"]
なんだか見た目がfor
文みたいになりましたが、制御構文ではなくて、あくまでもmap
メソッドに{}
内のクロージャを渡しているだけです。
なお、()
を省略できるのは、関数の引数がクロージャ一つだけで、Tailing Closureを使う場合に限られます。関数呼び出しhoge()
をhoge
と書いてもいい、というわけではありません。なかなか強引な仕様です。
引数を一つにする
ここまではタプルを展開した状態で受け取っていましたが、元々、マッピングクロージャは、配列の要素を一つだけ受け取るという定義でしたね。この後の説明と整合しなくなるので、ここで、ひとつのタプルとして扱うように直しておきます。
let list = [("Tokyo", 22), ("London", 18)]
let cityList = list.map { (element:(String,Int)) -> String in
return element.0
} // ["Tokyo", "London"]
ここで登場した.0
は、タプルの0番目の項目という意味です。ご存知ない方は、タプルを調べてみてください。
クロージャ定義の型を省略
{
とin
の間にmap
メソッドのクロージャの型定義がありますが、なんとこれらは省略可能です。
クロージャが受け取る引数の型は、map
メソッドの定義で配列の要素と同じと決まっているので、わざわざ書かなくても自動的に決まります。しかし、クロージャが返すT
は任意の型という定義でした。これを省略したら、コンパイルエラーになりそうです。
let list = [("Tokyo", 22), ("London", 18)]
let newList = list.map { (element) in
return element.0
} // ["Tokyo", "London"]
実は、swiftはクロージャの戻り値の型からT
を決める、という、賢い類推機能をもっています。element.0
がString型なので、map
はString型の配列を返します。returnの値をelement.1
にしたら、Int型の配列がmap
の出力になります。
おお、なるほどこれは簡単、便利で理にかなっているように思えます。
クロージャの引数名定義を省略
残っているのはelement
というクロージャ内で引数を参照するための変数名定義だけとなりました。そして、これも省略できます。
swiftでは、クロージャの引数について、$0
という暗黙の引数名を用いることができます。
let list = [("Tokyo", 22), ("London", 18)]
let newList = list.map { return $0.0 } // ["Tokyo", "London"]
$0
は shorthand argument name というもので、引数が複数あるときは、$1
,$2
,...というふうになります。また、element
を囲っていた()
も、in
も不要です。
return
を省略
最後に残ったreturn
ですが、これさえも省略してしまいます。
swiftには、クロージャの中身が一文の場合は、returnを省略できる、という、またまた強引なルールがあります。Inpricit returns from Single-Expression Closureと、仕様書で説明されています。このとき、return
がなくてもその一文の値を自動的に戻り値とする、という実に強引な仕様になっています。
let list = [("Tokyo", 22), ("London", 18)]
let newList = list.map { $0.0 } // ["Tokyo", "London"]
やっと、最初の表現にたどり着きました。
単純な表記の裏に、推定や暗黙の決まりがたくさんあります。
これだけの変換や省略を頭の中で組み立てて、さらっとコーディングできる皆さんは、なかなか大したものだと思います。
私には、やっぱり書くのは難しいですが、読む事はできるような気がしてきました。
map
を読解するポイント
コードに現れるmap
を読むときに意識するべきポイントをまとめます。
- マッピングクロージャには元の配列の値の一つが与えられる。
$0
でそれを参照すして使う。 - クロージャの実体である一文は、クロージャの戻り値になる。
- クロージャの戻り値が配列に集められて
map
の結果になる。
reduce
Array
のreduce
メソッドは以下の定義です。
func reduce<Result>(into initialResult: Result, _ updateAccumulatingResult: (inout Result, Element) throws -> ()) rethrows -> Result
reduceはupdateAccumelatingResult
の部分にクロージャを渡して、最終的にはResult型の値を一つだけ返します。
initialResultはResult型の初期値です。
クロージャには、Result型の値と、配列の要素の二つが渡され、戻り値はResult型です。
クロージャが受け取るResult型の値は、その前に呼び出されたクロージャの戻り値です。
一番最初のクロージャは、reduce
に与えたinitialResult
の値と、一番最初の配列要素を受け取ります。それが返したResult型の値が、二番目の配列要素と一緒に次のクロージャ呼び出しに使われ、これを最後の要素まで繰り返します。
最後のクロージャが返した値が、reduce
の結果になります。
順番に実行して結果を伝搬していく処理に向いていますね。全部足すとか。
以下の例はIntの配列を文字列に変換しています。
let list = [1, 2, 3, 4, 5]
let string = list.reduce ("") { $0 + "\($1), " }
// string = "1, 2, 3, 4, 5, "
reduce
の最初の引数は初期値です。最初の引数があるのでmap
みたいに()を省略することはできません。
$0
は初期値または前のクロージャの戻り値で、ここではString型です。
$1は配列の値なので、Int型です。この型の違いが、reduceのポイントかなと思いました。
三項演算子を使って、気持ちの悪いケツカンマを外して[]で囲んでみます。
let list = [1, 2, 3, 4, 5]
let string = list.reduce ("") { $0 == "" ? "[\($1)" : $0 + ", \($1)" } + "]"
// string = "[1, 2, 3, 4, 5]"
一行ですが、トリッキーで読みやすいとは言えないと思います。
書くときは分かっているから楽しいですが、あとで読むときには辛いですね。
reduce読解のポイント
-
reduce
の第一引数が初期値で、次がクロージャ。 - クロージャが受け取るのは、初期値または前のクロージャの戻り値、と、配列の値の二つ。
-
$0
で初期値またはクロージャの戻り値を参照し、$1
で配列の値を参照する。それぞれ型が違う場合がある。 - 最後のクロージャの出力が、reduceの出力になる。
flatMap
Array
のflatMap
の定義はこちら。
func flatMap<SegmentOfResult>(_ transform: (Element) -> SegmentOfResult) -> [SegmentOfResult.Element] where SegmentOfResult : Sequence
これはなかなかややこしい。
flatMap
は、クロージャを一つ受け取って、配列を返します。
クロージャは、配列の内容を一つずつ受け取って、SegmentOfResult型の値を返します。SegmentOfResult型は、Sequence
プロトコルに準拠していなければならない。[SegmentOfResult.Element]
という表記が私には謎ですが・・・
map
はクロージャの戻り値を単に配列化して返すので、クロージャで配列を返すと二次元配列になってしまうのに対して、flatMap
はクロージャの戻り値が一つの配列にフラットに展開されます。
let list = [("Tokyo", 22), ("London", 18)]
let map = list.map { Array($0.0) }
// map = [["T", "o", "k", "y", "o"], ["L", "o", "n", "d", "o", "n"]]
let flat = list.flatMap { Array($0.0) }
// flat = ["T", "o", "k", "y", "o", "L", "o", "n", "d", "o", "n"]
flatMap
の読解のポイント
-
map
と同じ。 -
map
の出力を二次元にしたくない場合に使う(みたい)
compactMap
func compactMap<ElementOfResult>(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult]
compactMap
は、map
のマッピングクロージャがnilを返す場合であって、最終結果の配列にnilを入れたくないときに使う、と説明されています。
クロージャがnilを返したらそれを出力に含めないmap
です。なんだ、簡単。
sorted
func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element]
勢いでsorted
もやってみます。
sorted
はクロージャを受け取って、新しい配列を返します。
要素二つをもらって、大小をBoolで返してあげたら、その順に並べ替えられた新しい配列ができます。
let list = [1, 3, 2, 5, 4]
let sortedList = list.sorted { $0 > 01 }
// sortedList = [5, 4, 3, 2, 1]
これは、
let list = [1, 3, 2, 5, 4]
let sortedList = list.sorted { (left:Int, right:Int) -> Bool in
return left > right
}// sortedList = [5, 4, 3, 2, 1]
こういう意味ですね。
だいぶ読めるようになりました。
終わり
Array
だけでなく、Dictionary
もmap
などの便利関数群がたくさん用意されています。だいぶわかるようになった気がするので、これを機にいろいろ使ってみようと思います。
高階関数とか、カリー化とか、関数型言語とか、もともとmap
やreduce
などが登場した背景技術には奥深いものがあるようです。考え方が根本からひっくり返るのがソフトウェアの醍醐味ですが、ここにもそれがありそうです。関数型言語はやったことがないので、機会を作って、是非勉強してみたいなと思います。
ジェネリクス(Generics)
ジェネリクスは具体的な型を決めないで、クラスや構造体を定義するための仕組みです。例えばArray
はジェネリクスを使って型に依存しない構造体として定義されています。Intの配列でもStringの配列でも、配列そのものに対する処理は共通であり、それらを型によらずに定義できるようにした技術です。
struct Array<Element>
<
と>
に挟んだElementが型名のかわりで、何かの型という意味です。慣習で<T>
とT
を使うことが多いと思うけれど、SwiftのドキュメントではElement
やSegmentOfResult
などの意味を表す単語も使っています。
ジェネリクスによって、IntのArrayもStringのArrayもまとめて定義でき、同じように使えます。自分の作ったクラスでも使える。ジェネリクスが登場する前は、Arrayクラスを継承で任意の型に派生させたり、プロトコルやインターフェースを使ったりして、全部の型のために定義をわざわざ書いていました。
もうひとつのreduce
Array
のreduce
にはもう一つのパターンがあります。
func reduce<Result>(into initialResult: Result, _ updateAccumulatingResult: (inout Result, Element) -> ()) -> Result
reduce
の最初の引数の引数名がinitialResult
から、into
に変わっています。そして、クロージャの1番目の引数にinout
属性が付きました。
このreduce
は、初期値を使いまわして結果まで再利用できるようです。
そういえば、この関数の説明の例題に、変な構文がでてきます。
let letters = "abracadabra"
let letterCount = letters.reduce(into: [:]) { counts, letter in
counts[letter, default: 0] += 1
}
// letterCount == ["a": 5, "b": 2, "r": 2, "c": 1, "d": 1]
[letter, default: 0]
と、辞書のキーを書くべきところに,default: 0
という見慣れない書き方が・・・
調べてみたら、Qiitaに説明がありました。ありがとうございます。
Swift4.0でDictionaryが大幅にパワーアップした
確かに、この構文があれば+=
で簡単に書けますね。使ってみよう。