Swiftのmap, filter, reduce(などなど)はこんな時に使う!
Swiftをさわり始めてある程度経つと「Swiftらしく書きたい」という欲望がわいてきます。そしてObjective-Cでは触れたことのない、map
, filter
, reduce
というのものを目にすることになると思います。
これらの関数を見たときの最初の印象は「うわ、何これ」といったもので、見慣れない構文に戸惑いました。同じように戸惑いを感じた方もいるのではないでしょうか。
特にどんなシチュエーションで、何を使えばよいのかといった部分があまりピンと来ず、なかなか使いこなせずにいました。
そのような経験を踏まえ、**こういう時は、これを使う!**と一言で説明することを目指してこの記事を書きました。
はじめに結論から
一言でまとめると、これらの関数を覚えるとめちゃくちゃ便利になります。
この関数はこんな時に使う!
個人的にこれから説明する関数を使うようになって感じたメリットをいくつか挙げてみました。
- 配列の操作が圧倒的にやりやすくなる
- 無駄な空配列を用意しなくて済む
- コードの書き方が変わる
- コードが短くなる
ただ、一方でなんでもかんでもmap
やfilter
でやってしまおうとする(書きたくなってしまう)のですが、Swiftにはほかにも便利な関数が用意されています。
本文中で説明しますが、例えばforEach
という関数があるのにもかかわらずmap
を使ってしまったりしていたことがありました。map
とかfilter
使いたい症候群になっていたのですが、やはり書きやすさや分かりやすさの点で、目的に適した関数を使うことが大事ですよね。
そんな経験を踏まえて、どんなシチュエーションで何を使えばよいのかについての指標があれば良いなと思いまとめてみました。
※なおflatMap
は一言で説明することが難しく、何ができるかの説明にとどまっています。こちらについては良い説明ができるようになったら追記したいと思います。
map
は全要素に処理を適用したい時に使う!
map
は配列内の要素に処理を適用し、その処理を施した配列を使いたい場合に使用します。
以下のコードでは、各要素を5倍した新しい配列newArray
をmap
で生成しています。
let array = [1,2,3,4,5]
let newArray = array.map { $0 * 5 }
newArray // [5, 10, 15, 20, 25]
map
には@warn_unused_result
というアトリビュートが指定されており、返り値が使用されていない場合にwarningを出します。戻り値が必要ないけど配列内の要素を使用して何か処理を行いたい場合は後述のforEach
を使用すると良いでしょう。
map
はSequenceType
の準拠した型であれば使用できるのでDictionary
にも使用できます。以下のコードでは摂氏を華氏に変換しています。
let celsius = ["Tokyo":14.0, "Osaka":17.0, "Okinawa":26.0]
let fahrenheit = celsius.map { ($0.0, 1.8 * $0.1 + 32) } // [("Okinawa", 78.8), ("Tokyo", 57.2), ("Osaka", 62.6)]
全要素に処理をかけたい場合にとても便利な関数です。
filter
は条件に合う要素を絞り込む時に使う!
filter
は、条件にマッチする要素のみを取り出したい場合に使用します。
以下のコードでは配列内の3未満の数値を取り出しています。
let array = [1,2,3,4,5]
let newArray = array.filter { $0 < 3 }
newArray // [1,2]
filter
にも@warn_unused_result
が指定されています。
以下のコードでは、map
で摂氏を華氏に変換してから、filter
で華氏60度以上の地域を絞り込み、map
で地域名だけを取り出しています。
let celsius = ["Tokyo":14.0, "Osaka":17.0, "Okinawa":26.0]
let fahrenheit =
celsius.map { ($0.0, 1.8 * $0.1 + 32) } // [("Okinawa", 78.8), ("Tokyo", 57.2), ("Osaka", 62.6)]
.filter { $0.1 > 60 } //[("Okinawa", 78.8), ("Osaka", 62.6)]
.map { $0.0 } // ["Okinawa", "Osaka"]
reduce
は要素をまとめて一つにしたい時に使う!
reduce
は、要素使って結果を集計したいような場合に使用します。
以下のコードでは配列内の要素を全て足し合わせる処理を行っています。第一引数には初期値となる値を入れます。
let array = [1,2,3,4,5]
array.reduce(0) { (num1, num2) -> Int in
num1 + num2
}
// 15
このように書くこともできます。
let array = [1,2,3,4,5]
array.reduce(0, combine: +) // 15
array.reduce(1, combine: *) // 120
以下のコードでは、map
で摂氏を華氏に変換してから、reduce
で全地域の温度を足し合わせたものを要素数で割り、華氏での平均気温を算出しています。
let celsius = ["Tokyo":14.0, "Osaka":17.0, "Okinawa":26.0]
let average = celsius.map { 1.8 * $0.1 + 32 } .reduce(0, combine: +) / Double(celsius.count) // 66.2
reduce
は一見分かりにくく慣れるのに手こずりそうですが、要素をまとめたい場合に非常に便利です。
forEach
はfor-in
をより簡潔に記述したい時に使う!
forEach
はforin
と同じように使用します。各要素に対して処理を行い、かつ戻り値が必要ない場合に使用すると良いと思います。
僕も最初やってしまっていたのですが、各要素に対して処理を行う必要があるが戻り値が必要ない場合でもmap
を使用することは可能です。戻り値の型をVoid
にすれば実現できます。
array.map { (num) -> Void in
print("\(num)")
}
ただしmap
は@warn_unused_result
というアトリビュートが付いているので、上記のコードではwarningが発生します。
このような場合はforEach
が適しているでしょう。
array.forEach { print("\($0)") }
enumerate
要素と要素のインデックスが欲しい時に使う!
enumerate
はインデックスと要素のタプルを返します。
let scores = [84, 76, 91, 62 ,80]
for (index, score) in scores.enumerate() {
print("index:\(index), score: \(score)")
// index:0, score: 84
// index:1, score: 76
// index:2, score: 91
// index:3, score: 62
// index:4, score: 80
}
forEach
を使用して、このように書くこともできます。
let scores = [84, 76, 91, 62 ,80]
scores.enumerate().forEach { print("index:\($0.0), score: \(0.1)") }
indexOf
を組み合わせればfor-in
やforEach
でもインデックスを取得することが可能ですが、enumerate
の方がかなり簡単に処理を書くことが可能です。
minElement
ormaxElement
は最小要素or最大要素を取り出したい時に使う!
minElement
とmaxElement
はそれぞれ最小要素と最大要素を取り出すために使用します。
以下のコードでは、scores
リストに入っている最小要素と最大要素を取り出しています。
let scores = [84, 76, 91, 62 ,80]
scores.minElement() // 62
scores.maxElement() // 91
minElement
とmaxElement
を使用しない場合でも以下のように書くことができます。
let scores = [84, 76, 91, 62 ,80]
scores.sort(<).first // 62
scores.sort(>).first // 91
minElement
、maxElement
の方がわかりやすいですね。
dropFirst
ordropLast
は先頭要素or最後の要素をカットしたい時に使う!
dropFirst
は先頭要素をカット、dropLast
は最後の要素をカットします。
let scores = [84, 76, 91, 62 ,80]
scores.dropFirst() // [76, 91, 62, 80]
scores.dropLast() // [84, 76, 91, 62]
なお、順番が保証されていないSet
やDictionary
で使用する場合は意図しないものがカットされる可能性があるので注意しましょう。
flatMap
は説明が難しい
flatMap
に関しては、具体的にこういうケースで使う!という自信を持った説明ができないため、どういうことができるかの説明にとどめます(良い解説方法をひらめいたら更新します)。
以下のコードでは、array
にInt
のOptionnal
型として入っているInt?
の要素をflatMap
を使用してOptional
ではないInt
に写しています。
let array: [Int?] = [1, nil, 41, 6, nil, 20, 451, 7]
let flatArray = array.flatMap{ $0 } // [1,41,6,20,451,7]
この場合のflatMap
はSequenceType
に定義されています。
extension SequenceType {
/// Return an `Array` containing the non-nil results of mapping
/// `transform` over `self`.
///
/// - Complexity: O(*M* + *N*), where *M* is the length of `self`
/// and *N* is the length of the result.
@warn_unused_result
@rethrows public func flatMap<T>(@noescape transform: (Self.Generator.Element) throws -> T?) rethrows -> [T]
}
説明を読むと、nil
ではない値をマッピングした配列を返す、と書いてあります。これを使えばOptional
(という文脈)に包まれた型を安全に扱うことができるようになります。
次に以下のコードでは、array
にString
のOptionnal
型として入っているString?
の要素を1つ目のflatMap
でString
に写し、2つ目のflatMap
でInt
に写しています。
let array: [String?] = ["1",nil,"41","abc",nil,"20","451","7"]
let flatArray = array.flatMap{ $0 } // ["1", "41", "abc", "20", "451", "7"]
.flatMap { Int($0) } // [1, 41, 20, 451, 7]
最初のflatMap
はnil
を取り除き、2つ目のflatMap
で各文字をInt
に変換しています。"abc"
は当然Int
にはならないので変換時にnil
が返ってきますが、flatMap
はnil
でない値の配列を返すため含まれていません。
こちらも見てみましょう。このarray
はOptional
ではありませんが、Int
のArray
、[Int]
が配列に包まれて[[Int]]
となっています。
let array: [[Int]] = [[1,41,6],[],[20,451,7]]
let flatArray = array.flatMap { $0 } // [1,41,6,20,451,7]
flatMap
で[[Int]]
が[Int]
となっています。この場合のflatMap
は以下のようになっておりflatMap
のtransform
の戻り値がSequenceType
の場合に、こちらになります。
extension SequenceType {
/// Return an `Array` containing the concatenated results of mapping
/// `transform` over `self`.
///
/// s.flatMap(transform)
///
/// is equivalent to
///
/// Array(s.map(transform).flatten())
///
/// - Complexity: O(*M* + *N*), where *M* is the length of `self`
/// and *N* is the length of the result.
@warn_unused_result
@rethrows public func flatMap<S : SequenceType>(transform: (Self.Generator.Element) throws -> S) rethrows -> [S.Generator.Element]
}
説明にもある通りこのコードは以下のように書くこともできます。
let array: [[Int]] = [[1,41,6],[],[20,451,7]]
let flatArray = array.map {$0} .flatten() // [1,41,6,20,451,7]
flatten
はself
の要素をつなぐ関数で以下のように定義されています
extension CollectionType where Generator.Element : CollectionType, Index : BidirectionalIndexType, Generator.Element.Index : BidirectionalIndexType {
/// A concatenation of the elements of `self`.
@warn_unused_result
public func flatten() -> FlattenBidirectionalCollection<Self>
}
では、[[Int?]]
なリストも見てみましょう。
let array: [[Int?]] = [[1,nil,6],[nil, nil],[20,451,7]]
let flatArray = array.flatMap {$0} // [Optional(1),nil,,Optional(6),nil,nil,Optional(20),Optional(451),Optional(7)]
.flatMap {$0} // [1,6,20,451,7]
まずはmap
+flatten
と同じ結果となるflatMap
が呼ばれ、[[Int?]]
が[Int]
となります。2つ目のnil
の要素を含まないリストを返すflatMap
でOptional
でないInt
に値が写されています。
flatMap
の参考にした記事を以下にまとめましたので、こちらもご参照ください。
flatMap
についての参考記事
- [Swift][Objective-C] 同期的な関数ハンドラを示す noescape ディレクティブ
- mapとflatMapという便利メソッドを理解する
- GeneratorとSequence
- 【Swift】配列の配列について詳しく。
- 【Scala】flatMap は怖くない!
具体例
ここでは、ここまでに紹介した関数を使用した具体例を紹介します。
具体例1: iOSで使用可能なフォントをすべて出力する
iOSのバージョンが上がるたびに使用可能なフォントを確認するためにフォント名の出力を行っていたと思いますが、これはこのように書くことができます。
UIFont.familyNames().map {UIFont.fontNamesForFamilyName($0)} .flatten() .sort() .forEach { print("\($0)") }
まずUIFont.familyNames()
でフォントファミリー名の配列を取得し、そのフォントファミリー名の配列に対してmap
を使用します。map
の結果から得られるのはフォントファミリー名の配列(=[String]
)です。
map
で適用する関数としてfontNamesForFamilyName
を使用し、フォントファミリーに属するフォント名の配列を取得します。
UIFont.familyNames().map {UIFont.fontNamesForFamilyName($0)}
上記処理までに取得できるものは、" "フォントファミリーに属するフォント名の配列" を格納した配列"(=[[String]]
)となっています。
これをflatten
を使用して[[String]]
から[String]
に変換し、最後にソートしています。
ここまでの説明で処理の流れがつかめないという方は、Playgroundを使用して各関数ごとに区切ってprint
で出力しながら確認してみると理解が進むと思います。
まとめ
Functionalに書こうと思い始めた時に、map
やfilter
やreduce
を使い始めたのですが、配列などの要素に対する操作がObjective-Cに比べてすごく扱いやすくなったと感じています。
一方でmap
やfilter
、その他の関数を組み合わせれば実現可能ではあるものの、「なんだかmap
とfilter
を使いたくて無駄なことをしているな」と感じたことが結構ありました。例えばforEach
という関数があるのにもかかわらずmap
を使ってしまったりしていたことがありました。
冒頭でも述べましたが、どんなシチュエーションで何を使えばよいのかについての、おおざっぱでも良いので、指標があれば良いなと思いまとめてみました。
ぜひ使い方を覚えて楽しいSwiftコーディングライフを送りましょう。