Swiftのmap, filter, reduce(などなど)はこんな時に使う!

  • 396
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Swiftのmap, filter, reduce(などなど)はこんな時に使う!

Swiftをさわり始めてある程度経つと「Swiftらしく書きたい」という欲望がわいてきます。そしてObjective-Cでは触れたことのない、map, filter, reduceというのものを目にすることになると思います。

これらの関数を見たときの最初の印象は「うわ、何これ」といったもので、見慣れない構文に戸惑いました。同じように戸惑いを感じた方もいるのではないでしょうか。

特にどんなシチュエーションで、何を使えばよいのかといった部分があまりピンと来ず、なかなか使いこなせずにいました。

そのような経験を踏まえ、こういう時は、これを使う!と一言で説明することを目指してこの記事を書きました。

はじめに結論から

一言でまとめると、これらの関数を覚えるとめちゃくちゃ便利になります。

この関数はこんな時に使う!

個人的にこれから説明する関数を使うようになって感じたメリットをいくつか挙げてみました。

  • 配列の操作が圧倒的にやりやすくなる
  • 無駄な空配列を用意しなくて済む
  • コードの書き方が変わる
  • コードが短くなる

ただ、一方でなんでもかんでもmapfilterでやってしまおうとする(書きたくなってしまう)のですが、Swiftにはほかにも便利な関数が用意されています。

本文中で説明しますが、例えばforEachという関数があるのにもかかわらずmapを使ってしまったりしていたことがありました。mapとかfilter使いたい症候群になっていたのですが、やはり書きやすさや分かりやすさの点で、目的に適した関数を使うことが大事ですよね。

そんな経験を踏まえて、どんなシチュエーションで何を使えばよいのかについての指標があれば良いなと思いまとめてみました。

※なおflatMapは一言で説明することが難しく、何ができるかの説明にとどまっています。こちらについては良い説明ができるようになったら追記したいと思います。

mapは全要素に処理を適用したい時に使う!

map配列内の要素に処理を適用し、その処理を施した配列を使いたい場合に使用します。

以下のコードでは、各要素を5倍した新しい配列newArraymapで生成しています。

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を使用すると良いでしょう。

mapSequenceTypeの準拠した型であれば使用できるので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は一見分かりにくく慣れるのに手こずりそうですが、要素をまとめたい場合に非常に便利です。

forEachfor-inをより簡潔に記述したい時に使う!

forEachforinと同じように使用します。各要素に対して処理を行い、かつ戻り値が必要ない場合に使用すると良いと思います。

僕も最初やってしまっていたのですが、各要素に対して処理を行う必要があるが戻り値が必要ない場合でも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-inforEachでもインデックスを取得することが可能ですが、enumerateの方がかなり簡単に処理を書くことが可能です。

minElementormaxElementは最小要素or最大要素を取り出したい時に使う!

minElementmaxElementはそれぞれ最小要素と最大要素を取り出すために使用します。

以下のコードでは、scoresリストに入っている最小要素と最大要素を取り出しています。

let scores = [84, 76, 91, 62 ,80]
scores.minElement() // 62
scores.maxElement() // 91

minElementmaxElementを使用しない場合でも以下のように書くことができます。

let scores = [84, 76, 91, 62 ,80]
scores.sort(<).first // 62
scores.sort(>).first // 91

minElementmaxElementの方がわかりやすいですね。

dropFirstordropLastは先頭要素or最後の要素をカットしたい時に使う!

dropFirstは先頭要素をカット、dropLastは最後の要素をカットします。

let scores = [84, 76, 91, 62 ,80]
scores.dropFirst() // [76, 91, 62, 80]
scores.dropLast()  // [84, 76, 91, 62]

なお、順番が保証されていないSetDictionaryで使用する場合は意図しないものがカットされる可能性があるので注意しましょう。

flatMapは説明が難しい

flatMapに関しては、具体的にこういうケースで使う!という自信を持った説明ができないため、どういうことができるかの説明にとどめます(良い解説方法をひらめいたら更新します)。

以下のコードでは、arrayIntOptionnal型として入っている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]

この場合のflatMapSequenceTypeに定義されています。

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(という文脈)に包まれた型を安全に扱うことができるようになります。

次に以下のコードでは、arrayStringOptionnal型として入っているString?の要素を1つ目のflatMapStringに写し、2つ目のflatMapIntに写しています。

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]

最初のflatMapnilを取り除き、2つ目のflatMapで各文字をIntに変換しています。"abc"は当然Intにはならないので変換時にnilが返ってきますが、flatMapnilでない値の配列を返すため含まれていません。

こちらも見てみましょう。このarrayOptionalではありませんが、IntArray[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は以下のようになっておりflatMaptransformの戻り値が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]

flattenselfの要素をつなぐ関数で以下のように定義されています

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の要素を含まないリストを返すflatMapOptionalでないIntに値が写されています。

flatMapの参考にした記事を以下にまとめましたので、こちらもご参照ください。

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に書こうと思い始めた時に、mapfilterreduceを使い始めたのですが、配列などの要素に対する操作がObjective-Cに比べてすごく扱いやすくなったと感じています。

一方でmapfilter、その他の関数を組み合わせれば実現可能ではあるものの、「なんだかmapfilterを使いたくて無駄なことをしているな」と感じたことが結構ありました。例えばforEachという関数があるのにもかかわらずmapを使ってしまったりしていたことがありました。

冒頭でも述べましたが、どんなシチュエーションで何を使えばよいのかについての、おおざっぱでも良いので、指標があれば良いなと思いまとめてみました。

ぜひ使い方を覚えて楽しいSwiftコーディングライフを送りましょう。

参考

この投稿は Swift Advent Calendar 20152日目の記事です。