[Swift] SwiftのmapとflatMapの動作を追ってみる

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは!
モバイルアプリサービス部の田中孝明です。

はじめに言っておきますと、iOS 10やSwift 3.0とはあまり関係無い内容です。
Swift 3.0に移行する上で、従来の機能をおさらいしておこうという内容のブログになります。

mapとflatMap

Swift 1.2から追加されたメソッドです。
Collection(Array)やOptionに備わっています。
このメソッドに共通する事は、「中身があるかもしれない」「なにかしらのオブジェクトの配列」などの状態を持つオブジェクトに対して、中身を操作する手段を提供しています。

Collectionに対するそれぞれの挙動

おそらくは一番馴染み深い方法ではないでしょうか。
mapはCollectionの中身に対して、処理した内容を愚直に「適用」していきます。

例えば以下のようなEnumがあったと仮定します。

enum Type: String {
    case grass    = "grass"
    case water    = "water"
    case fire     = "fire"
    case electric = "electric"
}

map

APIのレスポンス等で文字列で返ってきた値をパースする際、定義されているEnumに変換する処理を行ってみます。
これをmapでまず行ってみましょう。

let typeStrings = ["grass", "water", "fire", "electric"]
let types = typeStrings.map { Type(rawValue: $0) }

愚直に処理内容が適用されるので結果はTypeOptionalのArrayになります。

[Optional(Type.grass), Optional(Type.water), Optional(Type.fire), Optional(Type.electric)]

flatMap

次にflatMapで行ってみます。

let typeStrings = ["grass", "water", "fire", "electric"]
let types = typeStrings.flatMap { Type(rawValue: $0) }   

すると結果をアンラップして返す処理が働きます。

[Type.grass, Type.water, Type.fire, Type.electric]

map

Collectionのmapは愚直に適用していくので、nilが混じることもあります。

let typeStrings = ["grass", "water", "fire", "ice"]
let types = typeStrings.map { Type(rawValue: $0) }

// iceはTypeに定義されていないので、生成に失敗する
[Optional(Type.grass), Optional(Type.water), Optional(Type.fire), nil]

flatMap

対してCollectionのflatMapはnilの結果を無視しますので、mapでnilが混ざるようなケースを除外する事もできます。

let typeStrings = ["grass", "water", "fire", "ice"]
let types = typeStrings.flatMap { Type(rawValue: $0) }
// IceはTypeに定義されていないので、生成に失敗する
[Type.grass, Type.water, Type.fire]

双方それぞれ使いどころがあると思います。

Collection同士の処理に対するそれぞれの挙動

map

ではCollection(Array)同士のmap処理の結果を見てみましょう。

let types1 = ["electric", "electric", "fire"]
let types2 = ["water", "ice"]
let types3 = ["fire", "grass", "rock", "grass"]
let types = [types1, types2, types3].map { $0 }

これも愚直に適用処理が働きますので、以下のようなArrayが入れ子になる構造になります。

[["electric", "electric", "fire"], ["water", "ice"], ["fire", "grass", "rock", "grass"]]

flatMap

それではflatMapの場合はどうでしょうか。

let types1 = ["electric", "electric", "fire"]
let types2 = ["water", "ice"]
let types3 = ["Fire", "grass", "rock", "grass"]
let types = [types1, types2, types3].flatMap { $0 }

Collection同士に対するflatmap処理にはjoined処理が実行されるようになります。
そのため、結果が単純なArrayになります。

["electric", "electric", "fire", "water", "ice", "fire", "grass", "rock", "grass"]

Optionalに対するそれぞれの挙動

map

Optionalに対してもmapflatMapが実装されています。
mapは中身が存在する(Some)場合、中身に対しての処理の結果を返すようになります。

let value: String? = "fire"
let value1 = value.map { Type(rawValue: $0) }

この場合、処理内容によっては二重のOptionalになってしまうことがあります。

Optional(Optional(Type.fire))

flatMap

flatMapの場合は中身に対しての処理の結果に対してアンラップを行うようになりますので、二重のOptionalを返すようなことは無くなります。

let value: String? = "fire"
let value1 = value.flatMap { Type(rawValue: $0) }
Optional(Type.fire)

Optionalに対する操作を提供するということはOptional同士の処理を行う際に利用することもできるという事です。
例としてOptional同士の計算を行ってみます。

let value1: Int? = 10
let value2: Int? = 20
let value = value1.flatMap { v1 in 
    value2.map { v2 in v1 + v2 }
}

value1とvalue2がSomeの場合、それぞれ順番にflatMapで中身を操作し、mapで開示してきた値を処理した結果を返せば以下のような結果を得る事が出来ます。

Optional(30)


ただし、この方法は複数の数値を扱う場合は煩雑になります。
4つの数値の計算の場合を例にしてみます。

let value1: Int? = 10
let value2: Int? = 20
let value3: Int? = 30
let value4: Int? = 40

let value = value1.flatMap {
    v1 in value2.flatMap {
        v2 in value3.flatMap {
            v3 in value4.map {
                v4 in v1 + v2 + v3 + v4
            }
        }
    }
}
Optional(100)


flatMapを直列で処理したい場合にScalaとかではfor(yield)のようなシンタックスシュガーが提供されていたりするのですが、Swiftは残念ながらその機構がまだありません。

その他には?

非同期処理を直列で行いたい場合にflatMapを利用する事で簡潔に書く方法がありますが、こちらは外部ライブラリの助けを借りる必要があります。
この件に関しては こちらのブログで紹介したいと思います。

まとめ

mapflatMapの挙動の違いを書き記しました。
この内容に関しては勉強会などで断片的に発表してきたりしたのですが、説明不足や理解が足りない部分があったと思い、自分の復習も兼ねてまとめました。