6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

map, reduceなどをマスターしたい

Posted at

はじめに

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"]

おお。かっこいい。スッキリしています。
ですが、私には、cityListStringの配列になることが直感的に見えてきません。よくわからないけど、コメントを信じて次へ進むことしかできません。

理解するために、なぜこうなるのかを紐解いてみます

mapの定義

上述のmap関数は、Array構造体のメソッドです。定義は以下の通り。
(実際にはthrowsrethrowsなどのエラー制御用のキーワードがありますがここでは省略します)

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"]

$0shorthand 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

Arrayreduceメソッドは以下の定義です。

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

ArrayflatMapの定義はこちら。

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だけでなく、Dictionarymapなどの便利関数群がたくさん用意されています。だいぶわかるようになった気がするので、これを機にいろいろ使ってみようと思います。
高階関数とか、カリー化とか、関数型言語とか、もともとmapreduceなどが登場した背景技術には奥深いものがあるようです。考え方が根本からひっくり返るのがソフトウェアの醍醐味ですが、ここにもそれがありそうです。関数型言語はやったことがないので、機会を作って、是非勉強してみたいなと思います。

ジェネリクス(Generics)

ジェネリクスは具体的な型を決めないで、クラスや構造体を定義するための仕組みです。例えばArrayはジェネリクスを使って型に依存しない構造体として定義されています。Intの配列でもStringの配列でも、配列そのものに対する処理は共通であり、それらを型によらずに定義できるようにした技術です。

struct Array<Element>

<>に挟んだElementが型名のかわりで、何かの型という意味です。慣習で<T>Tを使うことが多いと思うけれど、SwiftのドキュメントではElementSegmentOfResultなどの意味を表す単語も使っています。
ジェネリクスによって、IntのArrayもStringのArrayもまとめて定義でき、同じように使えます。自分の作ったクラスでも使える。ジェネリクスが登場する前は、Arrayクラスを継承で任意の型に派生させたり、プロトコルやインターフェースを使ったりして、全部の型のために定義をわざわざ書いていました。

もうひとつのreduce

Arrayreduceにはもう一つのパターンがあります。

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が大幅にパワーアップした

確かに、この構文があれば+=で簡単に書けますね。使ってみよう。

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?