動機
flatMap
の振る舞いって難しいですよね...。特にArray
のflatMap
。
どのような値を返すのか突き止めるべく、あらゆるパターンの呼び出しを試して個別にその挙動を確認し、
自分独自の概念もこねくり回しながら、どうにか法則を抽出しようと試みる。
だけど、どうしてもその挙動を言葉に落とし込むことができなくて、涙で枕をぬらす夜が続きました。
ところがある日、そのソースコードを読んでみると、
あれだけ内部で複雑な動きをしているかに見えたflatMap
の振る舞いが、
(Array
の場合は)2つのオーバーロードと、意外なほど簡潔なコードだけで実現されていたことがわかったのです。
個別の現象からその内部構造を推測するトップダウン型
だけではなく、ソースコードに当たってその振る舞いを読み解くボトムアップ型
のアプローチも取り入れた結果、
これらのメソッドの振る舞いを、より多角的に理解できた気がしました。
で、これらに関連してcompactMap
なるメソッドが導入された新しいSwiftも正式リリースされて間もないことですし、そちらの話題もちょっぴり絡めつつ、まとめてみました。
この記事を読むことで、
Optional
だったり、複雑なArray
オブジェクトに対するmap
・flatMap
呼び出し、おまけにcompactMap
までも、その挙動が手に取るように把握できるようになります。
その結果、これらのメソッドに対する自信が深まり、Swiftがさらに楽しくなります。
他方、そもそもmap
やflatMap
とは何か、Swiftではどのように呼び出すのか...などといった事柄は、他の方の優れた解説に多くを任せようと思います。
この記事では、実際のソースコードを参照しながらflatMap
の挙動を解剖していくことで、
これまでとは別の角度からこのメソッドの理解を促進することに挑戦したいと思います。
Array
のflatMap
とcompactMap
が混在する世界線の話ができたら理想的なので、
よければSwift 4.1以降をご用意のうえ、お読みくださると嬉しいです。
環境
Xcode 9.3
Swift 4.1
用語・あるとよさげな知識
記事中で用いる用語の語義と、もし知っていればこの記事が遥かに読みやすくなると思われる知識について、あらかじめ共有させてください。
レシーバ
メソッドを呼び出す主体となるオブジェクトのこと。
メソッド呼び出しの記法 オブジェクト.メソッド()
の、.(ドット)
の左側。
let str = "HELLO"
str.lowercased() // レシーバは `str`であり、"HELLO"
let dog = Dog()
dog.bark() // レシーバは `dog`であり、Dog
let array = [1,2,3,4,5]
array.map{ $0 * $0 } // レシーバは `array`であり、[1,2,3,4,5]
あるとよさげな知識
SwiftOptional四方山話、
理論から入門するswift/lib/Sema - わいわいswiftc #1などにあるように、
オプショナル型とその非オプショナル型の間には、サブタイプ関係がある(T is T?)
// T is T?
let optionalInt: Int? = 1 // `T?`型の変数に、`T`型のオブジェクトを代入できている
また、Swiftの型の深淵をのぞくなどにあるように、
オプショナル型を戻り値にした関数オブジェクトとその非オプショナル型を戻り値にした関数オブジェクトの間には、サブタイプ関係がある(() -> T is () -> T?)
// () -> T is () -> T?
func bar() -> Int { return 1 }
let barOptional: () -> Int? = bar // `() -> Int?`型の変数に、`() -> Int` 型の関数オブジェクトを代入できている
ということを知っていると、特にOptional#map
、Optional#flatMap
周りの説明などが読みやすくなると思います。
もうひとつ、SwiftのOptional型を極めるの中などで用いられている、「オプショナルとは箱である」というアナロジーを、この記事を通して借用しています。もしリンク先をまだ読んだことがない場合、ぜひ読んでみてください。
またこの記事全編に渡り、上記の記事を参照いたしました。
Optional#flatMap(_:)の挙動を追う
標準ライブラリで定義された型のうち、flatMap
を持つ型はいくつかあると思いますが、
実際の開発などで最もよく使うものは、Optional
とArray
のそれではないかと思います。
そのため、本記事ではこの2つの型に属するflatMap
に絞って書いていきます。
では、まずはOptional
におけるflatMap
から。
Array
が持つflatMap
に比べると複雑度は低いと思われますが、
それでも深い理解を求めるのであれば、丁寧に見ていく必要があると考えます。
map
・flatMap
のきほん
そもそもmap
って、flatMap
ってどんな感じの処理なんだ、ってお話を簡単に。
map
map
・・・すなわちマッピング。
一般的なIT用語としてのマッピングなら、1
ある集合の個々の構成要素に対して、別の集合の要素を規則に従って機械的に対応付けたり割り当てたりすること。また、何かの分布や配置などを地図に重ね合わせて図示すること。
とりわけ、プログラミングにおけるマッピングなら、
あるオブジェクトをもとに、指定した変換ルールに基づき、新しいオブジェクトを生成すること。
こちらは引用というより、いま私が考えました。
変換ルールをクロージャとして指定してmap
に渡してあげることで、
その変換ルールに基づいて新たにオブジェクトを生成、返却してくれます。
たとえば、ある整数をもとに、その整数に1を足した整数を得たいなら、こんな感じ。
let num: Int? = 42
let newNum: Int? = number.map{ $0 + 1 }
print(newNum) // Optional("43")
flatMap
さて、そんな感じで使えるmap
メソッドなのですが、困ったこともあります。
例えばこんなケースだとどうでしょう。
標準ライブラリInt
には、文字列から整数を生成するイニシャライザが定義されています。2
public init?(_ description: String) {
self.init(description, radix: 10)
}
このイニシャライザはその定義にinit?
とあるように、
文字列から整数を生成できた場合、その整数を返しますが、
反対に生成できない場合、nil
を返します(失敗可能イニシャライザ: Failable Initializer
)。
つまり、このイニシャライザが返す値の型は、Int?
になります。
let num1: Int? = Int("11") // Optional(11)
let num2: Int? = Int("VA-11") // nil
// オプショナル型を返す処理を、非オプショナル型の変数で受けることはできない
let num3: Int = Int("11") // コンパイルエラー
最後の行にあるように、
オプショナル型を返す処理(メソッドや失敗可能イニシャライザ、他にもOptional Chaining
など)の結果は、必ずオプショナル型の変数で受ける必要がある(非オプショナル型の変数で受けることはできない)
という点は、一見当たり前のようで、何気にこのあと重要になるので、ぜひ知っておくとハッピーです。
(なお、そのようなルールになっている理由として、
もしオプショナル型を返す処理を非オプショナル型の変数で受けられるとしたら、
仮にその処理がnil
を返した場合、本来nil
を入れられない変数にnil
を格納できてしまい、
null安全が崩壊する...といった説明ができるのでは、と思います。)
話を戻して、このイニシャライザを用いてmap
を呼び出してみましょう。
let a: String? = "1"
let result: Int?? = a.map{ Int($0) }
print(result) // Optional(Optional(1))
ありゃ。map
を実行した結果、一重のOptional
ではなく、二重のOptional
で返ってきてしまいました。
このように、map
に渡したいクロージャが、その返り値にオプショナル型の値を返すクロージャだった場合、
map
の代わりにflatMap
を用いると、二重の箱をフラット(一重)にして結果を得られ、上手く対処できます。
let a: String? = "1"
let result: Int? = a.flatMap{ Int($0) }
print(result) // Optional(1)
map
の処理に加えて、フラットにする(Optional
という「箱」を、1個分ぺしゃんこにする)処理まで施してくれるのがflatMap
なのですね。
実装を見る
ではその実装を見てみましょう。
また、Optional
のflatMap
はmap
と対称性があり、比較しながら見ていくといっそう理解しやすくなるため、こちらも合わせて引用します(必要な箇所を抽出・編集しています)。
public enum Optional<Wrapped> {
case none
case some(Wrapped)
public func map<U>(
_ transform: (Wrapped) throws -> U // ※1 ここ同士が違う
) rethrows -> U? {
switch self {
case .some(let y):
return .some(try transform(y)) // ※2 ここ同士も違う
case .none:
return .none
}
}
public func flatMap<U>(
_ transform: (Wrapped) throws -> U? // ※1 ここ同士が違う
) rethrows -> U? {
switch self {
case .some(let y):
return try transform(y) // ※2 ここ同士も違う
case .none:
return .none
}
}
}
map
とflatMap
、交互ににらめっこしていると、
- 引数のクロージャの返り値 (※1)
- switch-case文の
case .some(let y)
内、return している箇所 (※2) ← 特に注目
だけが少し異なり、それ以外は全く同じつくりであることがわかります。
シグネチャこそ2つのジェネリクス型(Optional
に付随するジェネリクス型Wrapped
と、メソッド内で使われるジェネリクス型U
)が絡み、とっつきにくいのは確かなのですが、
より着目すべきはその本体({ }
の中身)であり、そこでなされている処理は意外なほどシンプルであることが見て取れます。
ではそのflatMap
の処理の中身を見てみましょう。
func flatMap<U>(
_ transform: (Wrapped) throws -> U?
) rethrows -> U? {
switch self {
case .some(let y):
return try transform(y)
case .none:
return .none
}
}
flatMap
を呼び出した主体(レシーバ)の値がnil
かどうかを判定し(switch self
)、
もしnil
であれば、nil
を返す。
一方nil
でなければ、値バインディングパターン(value-binding pattern)
のパターンマッチを用い(case .some(let y)
)、
列挙体Optional
のアソシエイト値として設定されている値(=**「オプショナルという箱の中身」**)を取り出して(let y
)、その値を引数で渡されたクロージャに適用する(try transform(y)
)。
最後に、クロージャの実行結果をflatMap
の結果として返す。
また、map
は処理のほぼすべてがflatMap
と同じなのですが、
1点だけ、クロージャの実行結果をオプショナルでラップした上で返す、という点だけが異なります。
func map<U>(
_ transform: (Wrapped) throws -> U
) rethrows -> U? {
switch self {
case .some(let y):
return .some(try transform(y)) // オプショナル型にラップした上で返す
case .none:
return .none
}
}
こうして見ると、「flatMap
がなんらかのフラットにする処理をしている」というよりも、
「map
が処理のあと、あえて値をオプショナルに包んでいる(.some(...)
)。flatMap
の"flat"とは、その対としての"flat"。」と考えていたほうが、おさまりが良いかもしれません。
「値をオプショナルでラップする」という余計なひと手間をわざわざ施しているぶん、
なんだかflatMap
よりもmap
のほうが特殊なメソッドのように感じられてきますね。
この動作を知るためには、その実装を覗くことなく到達するのはなかなか難しかったのでは...と思います。
注意点
さて、ここまであえて触れずにきたものの、気に掛けておかないといけない点があります。
上で見たように、flatMap
の場合map
と違って、
クロージャの実行結果がオプショナルに包まれることはありませんでした。
case .some(let y):
return .some(try transform(y)) // こちらは、クロージャの実行結果をオプショナル型でラップしているが...
...
case .some(let y):
return try transform(y) // こちらは、クロージャの実行結果を素直に返しているだけ
...
そのため、例えばこのflatMap
全体の戻り値の型は、非オプショナルな型となってもよさそうなものです。
let a: Int? = 1
let result = a.flatMap{ $0 + 1 } // 戻り値の型は `Int`、値は2かな...🤔
ですが、実際はその戻り値の型は、オプショナル型であるInt?
になってしまいます。
また、これを無理やりInt
型の変数で受けることはできません。
let a: Int? = 1
let result: Int? = a.flatMap{ $0 + 1 } // 実際は、型は `Int?`、値はOptional(2)になってしまった😯
let result: Int = a.flatMap{ $0 + 1 } // これはコンパイルエラー
どうしてこのような結果になってしまうのでしょう。
もう一度flatMap
のシグネチャに着目してみると、
引数として取るクロージャの定義に_ transform: (Wrapped) throws -> U?
とありますね。
public func flatMap<U>(
_ transform: (Wrapped) throws -> U? // ← ここに注目
) rethrows -> U? {
...
}
クロージャtransform
の戻り値は**「U?」**。
つまり、このクロージャを実行した結果返される値の型は、その定義にひきずられる形で、
オプショナル型であると解釈されてしまう(たとえ実際には「非」オプショナル型の値を返していたとしても)、というわけです。
そしてflatMap
がreturn
するのはこのオプショナル型の値なので、そうなるとそれを受け取る変数もオプショナル型でなければいけないのですね。
let a: Int? = 1
// `flatMap`に渡されたクロージャが返している値の型は、
// { return $0 + 1 } ...すなわち実際には `Int` 型のオブジェクト(今回は2)なのだが、
// クロージャの戻り値が `U?` になっているせいで、
// このクロージャを実行したとき返される値の型は `Int?`であると解釈されてしまう
let result: Int? = a.flatMap{ $0 + 1 }
また、よしんばtransform
の戻り値が「U?」
ではなく、「U」
、つまり非オプショナル型だったと仮定してみましょう。
public func flatMap<U>(
_ transform: (Wrapped) throws -> U // ← 実際はこうではなく、あくまで仮定です
) rethrows -> U? {
...
}
ですが仮にそうだったとしても、rethrows -> U?
とあるように、そもそも**flatMap
全体の戻り値も U?
、すなわちオプショナル型となっている**ため、結局はこのflatMap
の実行結果はオプショナル型の変数で受け取らないといけません。
public func flatMap<U>(
_ transform: (Wrapped) throws -> U // ← この戻り値が仮に `U` だったとしても...
) rethrows -> U? { // ← そもそも`flatMap`全体の戻り値が `U?`
...
}
これは、このようなコードを書くと確かめることができます。
// クロージャを受け取って実行し、その結果を返す関数
func myFunc<U>(
_ completion: () throws -> U // クロージャの戻り値の型は、非オプショナル型の `U` だが...
) rethrows -> U? { // この関数全体の戻り値の型は、オプショナル型の `U?`。なので...
return try completion()
}
// クロージャの戻り値が非オプショナル型だろうが、
// `myFunc`の戻り値が `U?` とオプショナル型になっているので、
// 結局このメソッドの結果は、オプショナル型の変数で受けないといけなくなる
let result1: Int? = myFunc{ return 1 }
// 当然非オプショナル型の変数で受けることはできず、コンパイルエラーになる
let result2: Int = myFunc{ return 1 } // error
そのため、どのみちflatMap
実行結果の戻り値はオプショナル型と解釈される、というわけなのでした。
flatMap
やmap
の結果がOptional
で返ってくるのは、
簡単に言うと「Optional
という箱の中身(値)に対して、それを箱から取り出すことなく(入れたまま)演算を行う」ことができると都合がよいから。
これにより、箱の中身(値)を外の世界に触れさせることなく、後続の処理につなげたりできるようになっています。
となると当然、返される値はOptional
という箱に入ったまま、というわけですね。
挙動のまとめ
・・・存外長い説明となってしまいました。
いま一度、Optional
のflatMap
の挙動をまとめておきます。
Optional#flatMap
・レシーバがnil
ならば、nil
を返す。
・レシーバがnil
でないならば、レシーバの"箱の中身"に対して引数で渡したクロージャを適用し、
その結果を返す。
(・加えて、その型定義に引きずられる形で、その結果はオプショナル型の変数で受ける必要がある。)
ついでに、Optional
のmap
の挙動は、こう説明できます。
Optional#map
・レシーバがnil
ならば、nil
を返す。
・レシーバがnil
でないならば、レシーバの"箱の中身"に対して引数で渡したクロージャを適用し、
結果をオプショナルに包んで、その結果を返す。
(・加えて、その型定義に引きずられる形で、その結果はオプショナル型の変数で受ける必要がある。
すなわち、クロージャ適用の結果得た値がオプショナル型であれば、二重のオプショナル型になる。)
以上、ソースコードを参照しながら、Optional
のflatMap
(と、map
)の仕組みについて考察しました。
(補足) Optional#map
・Optional#flatMap
に関する補足
...と言いつつ、この項ではOptional#map
・Optional#flatMap
にまつわる、
「さらに探求したかったので調べてみたけど、本筋からは外れる話題」について、補足としてまとめました。
次のArray#flatMap(_:)の挙動を追う(その1)
までスキップでも一切支障ございません。
Q1. (Wrapped) throws -> U?
に対し、なぜ(Wrapped) -> U
を渡せるのか?
Optional#flatMap
が引数として取るクロージャの型は (Wrapped) -> U?
です。
public func flatMap<U>(
_ transform: (Wrapped) throws -> U? // クロージャ`transform`の型は `(Wrapped) -> U?`
) rethrows -> U? {
...
}
にもかかわらず、このようなコードが書けてしまうのです。
let a: Int? = 1
let result: Int? = a.flatMap{ $0 + 1 } // `flatMap`に渡されたクロージャの返り値の型は`Int`
print(result) // Optional(2)
(Wrapped) -> U?
、つまりオプショナル型の返り値を返すクロージャを渡せ、と要求されているにもかかわらず、
(Wrapped) -> U
、すなわち非オプショナル型の返り値を返すクロージャを渡せています。
(Wrapped) -> U?
と(Wrapped) -> U
、2つの型はそれぞれ別のものだというのに、
なぜこのようなコードが許されるのでしょうか?
どうやらSwiftにおいて、() -> T
型の関数オブジェクトは、() -> T?
型のサブタイプとして振る舞うことが可能なルールになっています。
() -> T is () -> T?
そのため、() -> Int
型の関数オブジェクトを、flatMap
へ渡せていた、というわけです。
let a: Int? = 1
// `() -> Int` is `() -> Int?`なので、{ $0 + 1 } を`flatMap`に渡せていた!
let result: Int? = a.flatMap{ $0 + 1 }
前に見たように、map
ではなくあえてflatMap
を使うべき場面とは、**「クロージャの返り値の型がオプショナル型」**の場合です。
そのため、「クロージャの返り値の型がオプショナル型」ではない場合、flatMap
は本来使えるべきではなく、理想的にはコンパイルが通らないような仕組みになっているのが望ましいですが...。
このルールのせいで、実際にはコンパイルが通り、実行できてしまいます。
このことが後で触れるcompactMap
が新設された遠因になっていたりもするようで、その経緯についてはSwift 4.1で導入されたcompactMapとその背景について、こちらの記事がProposalをベースに簡潔に解説されていて、たいへん勉強になりました。
なお、Swiftにおいて2つの型の間にサブタイプ関係がある場合に実現可能になることは、
本記事の終盤に、「サブタイプ関係がある場合にできること」という見出しで、補足としてまとめてあります。
Q2. (Wrapped) -> U
に対し、なぜ(Wrapped) -> U?
を渡せる(ように見えてしまった)のか?
Optional#map
が引数として取るクロージャの型は (Wrapped) -> U
です。
public func map<U>(
_ transform: (Wrapped) throws -> U // クロージャ`transform`の型は `(Wrapped) -> U`
) rethrows -> U? {
...
}
にもかかわらず、このようなコードが書けてしまうのです。
let a: String? = "1"
let result = a.map{ Int($0) } // ...これ、`map`に渡されたクロージャの型は `(Wrapped) -> U?`。なんで渡せるんだ...?🤨
これが許されるのはちょっと奇妙な気がします。
Q1.
で既に見たように、引数の型 (Wrapped) -> Int?
に対し (Wrapped) -> Int
を渡せるのはわかりました。
が、ここで起こっていることはその反対で、引数の型 (Wrapped) -> Int
に対して、 (Wrapped) -> Int?
のクロージャを渡せているかのように見えます。
ある2つの型がサブタイプ関係にある場合において、サブタイプになっている型に対しスーパータイプの型を渡すことは、本来できないはずなのでは...?
なぜこのようなコードが許されるのでしょうか?
...結論から言えば、当然私の考え方が間違っていたのです。
私の思考の誤ちを開陳させていただきたく思います。
今回のミソは、クロージャの戻り値が( Int
のような一般的な型ではなく)U
というジェネリクス型であり、コンパイル時に具体的な型に置き換えられる、という点にありました。
上で見たように、今回map
に渡されたクロージャの型は(String) -> Int?
です。
let a: String? = "1"
let result = a.map{ Int($0) } // クロージャの型は`(String) -> Int?`
するとその結果、map
が持つ型パラメータ U
が Int?
に決定(特殊化)されることになります
(map
のシグネチャに出現するU
の箇所に着目して型パズルを解くと、論理的にInt?
に定まることがわかります)。
そのときの状態はこうなります。
```swift:map
が持つ型パラメータ`U`が特殊化された後の、今回の`map`の状態
// もともと`U`があった場所を`Int?`で置換すると、こうなる。
// 論旨とは全く関係ないが、`Wrapped`も`String`で特殊化された。
public func map(
_ transform: (String) throws -> Int?
) rethrows -> Int?? {
...
}
クロージャ`transform`が返す値の型は`Int?`、実際のコードで`map`に渡されたクロージャが返す値の型も`Int?`。(`Int($0)`)
よって、サブタイピング関係がどうのなんて話は初めから存在せず、`map`に渡されるクロージャにおいて型の整合性は保たれていたので、問題なくコンパイルできていた、というわけだったんですね。
**「クロージャが返す値の型は `U` って書いてあって、`U?` のように`?`が付いていないのだから、このジェネリクス型は絶対にオプショナル型にはならない!」**と思い込んでしまったが故に陥ったミスでした。
また、`U?` = `Optional<U>` という定義を柔軟に取り出せなかったことも、ドツボにはまっていった原因でした。
ここまでで判明したように、実際にはジェネリクス型に`?`が付いていなくても、それがオプショナル型で特殊化されることはありえます。
今回`U`が特殊化された型は`Int`ではなく、`Int?(Optional<Int>)`だったのです。
#### Q3. `U?` を返すメソッドで、なぜ `U??` を返せる(ように見えてしまった)のか?
`Q2.`の問題を考えていた際、同時に陥っていた問題がこちらです。
こちらも`Q2.`の解決と同時に解決しましたが、また忘れないように、文章にして残しておきます。
`Optional#map`が返す値の型は `U?` です。
```swift
public func map<U>(
_ transform: (Wrapped) throws -> U
) rethrows -> U? { // `map`が返す値の型は `U?`
...
}
にもかかわらず、このようなコードが書けてしまうのです。
let a: String? = "1"
// `map`が返す値の型は`Int??`.
// これって`map`の返す値の型は`U?`ということを考えると許容されないのでは...?🤨
let result: Int?? = a.map{ Int($0) }
print(result) // Optional(Optional(1))
map
の戻り値の型 U?
にもかかわらず、Int??
という二重のオプショナルの値を返せている?
通常のサブタイプ関係を鑑みるなら、戻り値 U??
に対し、実際にはU?
を返せるのはOKだけど、ここではその逆が成立している。
これはおかしいのではないか?と考えてしまいました。
...この勘違いも、Q2.
の疑問が解けたと同時に払拭できました。
今回の場合、map
が持つジェネリクス型 U
が、Int?
で解決(特殊化)されているので、map
はこうなっていたのでした(上記コードの再掲)。
```swift:map
が持つ型パラメータ`U`が特殊化された後の、今回のmapの状態(再掲)
// もともと`U`があった場所を`Int?`で置換すると、こうなる。
public func map(
_ transform: (String) throws -> Int?
) rethrows -> Int?? {
...
}
`map`の戻り値の型は`Int??`、実際の呼び出しの結果も `Int??`。
型の整合性はなんの問題もなく保たれていましたね。
```swift
let a: String? = "1"
let result: Int?? = a.map{ Int($0) } // 私の勘違い。実際は型の整合性は保たれていた😅
print(result) // Optional(Optional(1))
Q2
・Q3
の教訓として、**ジェネリクス型は当然Optional<Wrapped>
型としても特殊化される場合がある。シグネチャの?
の有無だけに捕らわれるな!**と自身に言い聞かせておきます。これでもう間違えるまい。
Array#flatMap(_:)の挙動を追う(その1)
本当はこちらが本日のメインディッシュです。
Optional
に続き、Array
のflatMap
についてもその挙動を追っていきます。
実装を見る
さっそくソースコードを確認してみましょう。3
Array
のflatMap
はArray
に直接書かれているのではなく、Sequence
のプロトコル拡張として定義されているようです。
(必要な箇所を抽出・編集しています。)
extension Sequence {
...
public func flatMap<SegmentOfResult : Sequence>(
_ transform: (Element) throws -> SegmentOfResult
) rethrows -> [SegmentOfResult.Element] {
var result: [SegmentOfResult.Element] = []
for element in self {
result.append(contentsOf: try transform(element))
}
return result
}
...
}
extension Sequence {
...
// @available(swift, deprecated: 4.1, renamed: "compactMap(_:)",
// message: "Please use compactMap(_:) for the case where closure returns an optional value")
public func flatMap<ElementOfResult>(
_ transform: (Element) throws -> ElementOfResult?
) rethrows -> [ElementOfResult] {
return try _compactMap(transform)
}
...
}
ここからわかるように、ArrayのflatMap
には2種類が存在し、
その呼び出され方によって、どちらが使われるかが決定される、というわけだったのですね。
flatMap
の呼び出し方によって、使われているflatMap
の種類がそもそも違っていたため、
自分にとってその挙動を見抜くことが困難を極めていたのも、1つにはこの点が原因だったのでした。
どのような場合に、どちらのflatMap
が呼ばれるのか?
その決定方法については後で改めて触れるとして、
まずは前者の**flatMap<SegmentOfResult : Sequence>
**の挙動を追ってみたいと思います。
public func flatMap<SegmentOfResult : Sequence>(
_ transform: (Element) throws -> SegmentOfResult
) rethrows -> [SegmentOfResult.Element] {
var result: [SegmentOfResult.Element] = []
for element in self {
result.append(contentsOf: try transform(element))
}
return result
}
Optional#flatMap
と同様、こちらもシグネチャの圧がやや強めですが、
そちらは一旦無視で実装のみに着目すると、その中身はわずか5行、かつその意図は非常に明確です。
あれだけクールなメソッドがたった5行で実装されていたなんて・・・ますますクールだ。
public func flatMap<SegmentOfResult : Sequence>( ... ) {
// その中身は、たった5行だけ
var result: [SegmentOfResult.Element] = []
for element in self {
result.append(contentsOf: try transform(element))
}
return result
}
その中身について読み解いていきましょう。
配列の各要素を取り出して(
for element in self
)、引数で渡されたクロージャに適用し(try transform(element)
)、その結果を一時変数に格納する(result.append(...)
)。
これをすべての要素に対して繰り返し、最後にその一時変数(新しい配列)を返す。
関数型プログラミングの香りを併せ持つとも称されるであろうこのメソッドですが、
その内部をひも解いていけば、実はバリバリ手続き型チックな記述で構成されていたこともわかりますね。
以下の例は、2次元配列([[Int]]
)をもとに、その中の要素([Int]
)をフラットにした新しい配列を返しています。
let result = [[1,2,3],[4,5]].flatMap{ $0 }
print(result) // [1,2,3,4,5]
以降、当記事ではこのflatMap
を、便宜上**「A) 配列をフラットにするflatMap
」**と呼びます。
フラットにするのはappend(contentsOf:)
の仕事
さて、このflatMap
はその内部でappend(contentsOf:)
というメソッドを使用していたのでした。
func flatMap<SegmentOfResult : Sequence>(
_ transform: (Element) throws -> SegmentOfResult
) rethrows -> [SegmentOfResult.Element] {
var result: [SegmentOfResult.Element] = []
for element in self {
result.append(contentsOf: try transform(element)) // ←ここ
}
return result
}
append(contentsOf:)
は配列を引数に取り、その要素をレシーバに追加するメソッドです。4
ここでなにげに重要なのが、append(contentsOf:)
の引数として渡された配列は、レシーバにそのまま加わるのではなく、
(配列という)その「箱」がつぶされた上で加わる、という点です。
var array = [1,2,3]
array.append(contentsOf: [4,5])
// [1,2,3,[4,5]] ではなく、[4,5]を格納する箱がつぶされ(フラットにされ)た上で、レシーバに追加される
print(array) // [1,2,3,4,5]
ひるがえって、このようなflatMap
呼び出しを追ってみると・・・
let array = [[1,2,3],[4,5]] // このような二次元配列があったとき・・・
let result = array.flatMap{ $0 } // それをそのままフラットにした新しい値を得たい
print(result) // ???
レシーバarray
の要素は[1,2,3]
と[4,5]
の2つなので、flatMap
内部のforループは2回転。
1回転めが終わった時点の状態をトレースすると、
append(contentsOf:)
の働きにより、内部の一時変数result
はこんな感じで・・・
```swift:flatMap
内部
func flatMap() {
var result: [SegmentOfResult.Element] // [1,2,3]
for element in self {
result.append(contentsOf: ...)
}
...
}
2回転めが終わった時点だと、こんな感じ。
```swift:`flatMap`内部
func flatMap() {
var result: [SegmentOfResult.Element] // [1,2,3,4,5]
for element in self {
result.append(contentsOf: ...)
}
...
}
よって、最終的な結果はこうなります。
let array = [[1,2,3],[4,5]]
let result = array.flatMap{ $0 }
print(result) // [1,2,3,4,5]
つまり、こちらのflatMap
が持つ「要素をフラットにした上で結果を返す」という性質は、
この**append(contentsOf:)
**こそが担っていた、というわけですね。
Array#append
には、append(contentsOf:)
に加え、append(_:)
という同名のメソッドが存在します。5
そしてこちらのappend(_:)
には、引数に取った配列をフラットにした上でレシーバに加えるという機能はありません。
var array = [[1,2,3]]
array.append([4,5]) // `append(contentsOf:)`ではなく、`append(_:)`を呼ぶ
print(array) // [[1,2,3],4,5] ではなく、[[1,2,3], [4,5]]
後述するもう一方のflatMap
が内部で使用しているのは、このappend(_:)
のほうです。
そしてこの違いこそが、2つのflatMap
の挙動の違いを生んでいたのです。
また後ほどArray#flatMap、Array#compactMapの注意点
で言及します。
注意点
後述したいと思います。
挙動のまとめ
改めて、こちらのflatMap
の挙動をまとめます。
Array#flatMap<SegmentOfResult:Sequence
>
配列の各要素を取り出して、引数で渡されたクロージャに適用し、その結果を一時変数に格納する。
これをすべての要素に対して繰り返し、最後にその一時変数(新しい配列)を返す。
Array#flatMap(_:)の挙動を追う(その2)
さて、Array
が持つflatMap
は、同名で別のものがありました。
なので、次はそちらの**flatMap<ElementOfResult>
**の挙動を追ってみたいと思います。
(あらかじめ言ってしまうと、Swift 4.1でcompactMap
にリネームされたflatMap
のことです。)
実装を見る
// @available(swift, deprecated: 4.1, renamed: "compactMap(_:)",
// message: "Please use compactMap(_:) for the case where closure returns an optional value")
public func flatMap<ElementOfResult>(
_ transform: (Element) throws -> ElementOfResult?
) rethrows -> [ElementOfResult] {
return try _compactMap(transform)
}
中身はたった1行、(アンダーバー)compactMap
という別のメソッドに、その仕事を完全に横流ししています。
気を取り直して、(アンダーバー)compactMap
の定義を見てみると...
func _compactMap<ElementOfResult>(
_ transform: (Element) throws -> ElementOfResult?
) rethrows -> [ElementOfResult] {
var result: [ElementOfResult] = []
for element in self {
if let newElement = try transform(element) {
result.append(newElement)
}
}
return result
}
どうやらこちらが実質的な本体のようですね。
といっても、こちらも1つ前に見た「(A) フラットにするflatMap
」と非常に似ています。
// 前回見たやつ
func flatMap<SegmentOfResult : Sequence>( ... ) {
var result: [SegmentOfResult.Element] = []
for element in self {
result.append(contentsOf: try transform(element))
}
return result
}
// 今回のやつ
public func flatMap<ElementOfResult>( ... ) {
return try _compactMap(transform)
}
func _compactMap<ElementOfResult>( ... ) {
var result: [ElementOfResult] = []
for element in self {
if let newElement = try transform(element) {
result.append(newElement)
}
}
return result
}
詳細を追ってみると、前回見たflatMap
と異なる点は、
・
append
されるのは「クロージャを適用した結果得た値がnil
でない場合のみ」という条件付きになったこと
・append
の方法がappend(contentsOf:)
ではなくappend(_:)
になったこと
のみですね。とりあえず使ってみましょうか。
以下の例は、
「整数の文字列が格納された配列をもとに、それを整数に変換した新しい配列を取得したい。
ただし、整数への変換に失敗した場合は、その要素をドロップしたい(含めたくない)"。」
と考えた人が書いたコードとなります。
let result = ["1", "two", "3"].flatMap{ Int($0) }
print(result) // [1, 3]
またこちらの例は、
「名字が格納された配列をもとに、それぞれの名字に"Kun(くん)"が付加された新しい配列を取得したい。
ただし、名字が特定の文字から始まっていた場合は、その要素をドロップしたい(含めたくない)"。」
と思い立ち、書いたコードとなります。
let result = ["Amemiya", "Sakamoto", "Kitagawa"].flatMap{
guard !$0.hasPrefix("A") else { return nil }
return $0 + " Kun"
}
print(result) // ["Sakamoto Kun", "Kitagawa Kun"] ← "A"から始まる名字はドロップされている
これらの例から察するに、こちらのflatMap
は、
クロージャ実行の結果がnil
になった場合、それをドロップした上で新しい配列を返すflatMap
、といった感じになっています。
またこれを別の角度から見ると、特定の条件下でドロップしたい要素がある場合は、
その条件のときにnil
を返すようにクロージャ内を記述してあげるのが使い方のコツといえそうです。
以降、当記事ではこのflatMap
を、便宜上**「B) nil
をドロップするflatMap
」**と呼称します。
また、上記で引用したコード定義内にあるコメントや、その定義の中身で誘導されるように、
こちらのflatMap
は、Swift 4.1で非推奨となり、compactMap
にリネームされました。
"compact"とはすなわち、nil
をドロップすることのようです。
注意点
後述したいと思います。
挙動のまとめ
というわけで、こちらのflatMap
(compactMap
)の挙動をまとめます。
Array#flatMap<ElementOfResult
> (compactMap
)
配列の各要素を取り出して、引数で渡されたクロージャに適用する。
その結果がnil
ではなかった場合に限り、その結果を一時変数に格納する。
(逆に言うと、結果がnil
だった場合、これはドロップされる。ここがミソ)
これをすべての要素に対して繰り返し、最後にその一時変数(新しい配列)を返す。
Array#flatMap
、Array#compactMap
の注意点
A) 配列をフラットにするflatMap
の注意点
・「A) 配列をフラットにするflatMap
」は、要素の要素がnil
だった場合、それをドロップしてくれるわけではありません。
「(B)」の方の挙動が頭に残っている場合、それにひきずられないようご注意。
// (A)
let result = [[1, nil], [3]].flatMap{ $0 }
print(result) // [Optional(1), nil, Optional(3)] ← `nil`がドロップされていない
B) nil
をドロップするflatMap
(=compactMap
)の注意点
・また反対に、「(B) nilをドロップするflatMap
(= compactMap
)」には、
「A) 配列をフラットにするflatMap
」が持つ、要素をフラットにした上で返す という性質は持ち合わせていません。
「(A)」の方の挙動が頭に残っている場合、それにひきずられないようご注意。
// (B)
let result = [1, 2, [3]].compactMap{ $0 }
print(result) // [1,2,[3]] ← [3] という「箱」が、フラットになっていない
こちらについては前に触れたように、Array#append
はオーバーロードによって2種類が存在し、それぞれその挙動が違うから、ですね。
(A) のflatMap
が内部で使用しているappend(contentsOf:)
は、引数の値をフラットにした上で内部の一時変数に追加するのに対し、
(B) のflatMap
(=compactMap
)が内部で使用しているappend(_:)
には、そのような効果はないのでした。
というわけで、ここまでソースコードを読んできた私たちにとっては、これらの挙動も実は知っての通りでしたね。
(ウソです。私はこの点しこたま混乱し倒した上、この先また忘れそうだから私のために書きました)
どのような場合に、どちらのflatMap
が呼ばれるのか
Array
オブジェクトに対しflatMap
を呼び出した際、どちらのflatMap
が呼び出されるのか、
その決定方法を考察してみましょう。
(Xcode
のサポートを受けられるため、開発中これらの事柄に思考を巡らす必要は一切ありません。)
「A) 配列をフラットにするflatMap
」に決定されるための条件
まず、「A) 配列をフラットにするflatMap
」になるための条件から考えてみましょう。
その定義に型パラメータ<`SegmentOfResult : Sequence`> とあるので、これがキーになってきそうです。
func flatMap<SegmentOfResult : Sequence>( ← ここ
_ transform: (Element) throws -> SegmentOfResult
) rethrows -> [SegmentOfResult.Element] {
...
}
まず、SegmentOfResult : Sequence
なので、SegmentOfResult
になる型がSequence
に適合していないといけないことが確定します。
それに加え、この型パラメータがどこで出現してどう使われているのか、定義とにらめっこしてみると、
flatMap
が取るクロージャtransform
の返り値に、SegmentOfResult
が出現していることがわかります。
func flatMap<SegmentOfResult : Sequence>(
_ transform: (Element) throws -> SegmentOfResult ← ここ
) rethrows -> [SegmentOfResult.Element] {
...
}
この2つを組み合わせて考えると、
flatMap
が取るクロージャの戻り値がSequence
に適合している場合、
こちらの「A) 配列をフラットにするflatMap
」に決定する、ということになりますね。
例えば、これなんかがそうです。6
let numbers = [1, 2, 3]
let flatMapped = numbers.flatMap { Array(repeating: $0, count: $0) }
print(flatMapped) // [1, 2, 2, 3, 3, 3]
クロージャの戻り値の型はArray
、とうぜんSequence
に適合してるので、
「A) 配列をフラットにするflatMap
」のほうが呼び出されています。
(コードを見ただけでは確証が持てない場合でも、
alt
+クリックでヘルプを表示し、どちらが呼び出されているか確認すれば大丈夫です。)
###「B) nil
をドロップするflatMap
(=compactMap
)」に決定されるための条件
一方、「B) nil
をドロップするflatMap
(=compactMap
)」になる場合ですが...こちらはここまで来たら簡単です。
すなわち、A)の場合と反対に、flatMap
(=compactMap
)が取るクロージャの戻り値がSequence
に適合していない場合、
こちらのflatMap
(=compactMap
)が呼ばれることになります。
例えば、これなんかがそうです。
// 'flatMap' is deprecated: Please use compactMap(_:) for the case where closure returns an optional value
let flatMapped = ["1", "two", "3"].flatMap{ Int($0) }
print(flatMapped) // [1, 3]
クロージャの戻り値の型はInt?
、Sequence
には適合していません。
そのため A) ではなく、「B) nil
をドロップするflatMap
(=compactMap
)」のほうが呼び出されています。
もちろんこの場合、flatMap
じゃなくてcompactMap
を使え、と警告されます。
将来的にはエラーになりそうだし、素直に従っておきます。
let compactMapped = ["1", "two", "3"].compactMap{ Int($0) } // ←`compactMap`に変更
print(compactMapped) // [1, 3] ← 結果はもちろん同じです
以上、どんな場合に、どちらのflatMap
が使われるか、を考察してきました。
最初にお伝えしたように、通常こんなことを毎回考える必要は全くなくて、
上の例にあるようにcompactMap
を使うべきシーンでflatMap
と書いた場合、
Xcode
がcompactMap
を使うように警告を出してくれて、簡単にリネームできるようになっています。
(補足) サブタイプ関係がある場合にできること
前項までで本筋の内容は終了しました。おわりに
までスキップでも一切支障ございません。
また、この項は後日、独立した記事としてくくり出したいと思います。
Swiftにおいて、2つの型のあいだに**サブタイプ関係
がある場合に可能になること**をまとめてみました。
それぞれの「できること」について、
A) 以下のような継承関係を持つ独自型 (Dog is Animal
)
class Animal {}
class Dog: Animal {}
print(Dog() is Animal) // true
と、
B) ある非オプショナル型は、そのオプショナル型のサブタイプ(T is T?
)
print(1 is Int?) // true
という、2つのサブタイプ関係を例にとって見ていきます。
1: スーパータイプの変数に、サブタイプのインスタンスを代入できる
// 【Animal - Dog】パターン
let animal: Animal = Dog()
// 【T? - T】パターン
let optionalInt: Int? = 1
2: 関数がスーパータイプの引数を要求するとき、サブタイプのインスタンスを渡せる
// 【Animal - Dog】パターン
func takeAnimal(_ animal: Animal) { }
takeAnimal(Dog())
// 【T? - T】パターン
func takeOptional(_ optional: Int?) { }
takeOptional(1)
3: 関数の戻り値の型がスーパータイプのとき、サブタイプのインスタンスを返せる
// 【Animal - Dog】パターン
func getAnimal() -> Animal {
// 関数の返り値は`Animal`だが、サブクラスである`Dog`オブジェクトを返せる
return Dog()
}
// 関数が返した`Dog`オブジェクトは、代入時に関数の戻り値の型へ型強制(暗黙的なアップキャスト)される
let animal: Animal = getAnimal()
// 変数に格納されたオブジェクトの型は`Dog`なので、もちろんダウンキャストには成功する
print(animal as! Dog) // 成功
//【T? - T】パターン
func getOptional() -> Int? {
// 関数の返り値は`Optional<Int>`だが、サブタイプである`Int`オブジェクトを返せる
return 1
}
// 関数が返した`Int`オブジェクトは、代入時に関数の戻り値の型へ型強制(暗黙的なアップキャスト)される
let optionalInt: Int? = getOptional()
// 変数に格納されたオブジェクトの型は`Int`なので、もちろんダウンキャストには成功する
print(optionalInt as! Int) // 成功
4: 親クラスのメソッドの「スーパータイプの戻り値」を、子クラスのメソッド定義内で「サブタイプの戻り値」を返すようにオーバーライドできる
// 【Animal - Dog】パターン
class SuperClass1 {
func foo() -> Animal { return Animal() }
var bar: Animal { return Animal() }
var baz: () -> Animal {
return { return Animal() }
}
}
class SubClass1: SuperClass1 {
// サブタイプの戻り値を返すようにオーバーライドできる(Dog is Animal)
override func foo() -> Dog { return Dog() }
// コンピューテッドプロパティでも成立する
override var bar: Dog { return Dog() }
// もしや、と思ったら...こう書ける。マジかw
override var baz: () -> Dog {
return { return Dog() }
}
}
// 【T? - T】パターン
class SuperClass2 {
func foo() -> Int? { return Optional.some(1) }
var bar: Int? { return Optional.some(1) }
var baz: () -> Int? {
return { return Optional.some(1) }
}
}
class SubClass2: SuperClass2 {
// サブクラスの型を返すようにオーバーライドできる(Int is Optional<Int>)
override func foo() -> Int { return 1 }
// コンピューテッドプロパティでも成立する
override var bar: Int { return 1 }
// もしや、と思ったら...こう書ける。マジかw
override var baz: () -> Int {
return { return 1 }
}
}
また、もし2つの型の間にサブタイピング関係があるなら...
if T is Optional<T>
if Dog is Animal
それらの型を戻り値に置いた関数オブジェクトにおいても、サブクラス関係が成立する、といえます。
then () -> T is () -> Optional<T>
then () -> Dog is () -> Animal
よって、以下のようなコードが成立します。
(もしかすると「類型5」としてではなく「類型1や2や3の亜種」と考えた方が自然に感じられるかもしれません。)
5-1: () -> スーパータイプ
の変数に、() -> サブタイプ
の関数オブジェクトを代入できる
(類型1の亜種とも考えられる)
// 【Animal - Dog】パターン
func foo() -> Dog { return Dog() }
let fooAnimal: () -> Animal = foo // `() -> Animal`型の変数に、`() -> Dog` 型の関数オブジェクトを代入できている
// 【T? - T】パターン
func bar() -> Int { return 1 }
let barOptional: () -> Int? = bar // `() -> Int?`型の変数に、`() -> Int` 型の関数オブジェクトを代入できている
5-2: () -> スーパータイプ
を要求する引数に対し、() -> サブタイプ
の関数オブジェクトを渡せる
(類型2の亜種とも考えられる)
// 【Animal - Dog】パターン
func takeClosure(_ completion: () -> Animal) { }
func getDog() -> Dog { return Dog() }
takeClosure(getDog) // `() -> Animal` 型の引数に対し、`() -> Dog` の関数オブジェクトを渡せている
takeClosure{ Dog() } // ↑の2行を手短に書くとこう。やってることは同じ
// 【T? - T】パターン
func takeClosure(_ completion: () -> Int?) { }
func getOne() -> Int { return 1 }
takeClosure(getOne) // `() -> Int?` 型の引数に対し、`() -> Int` の関数オブジェクトを渡せている
takeClosure{ 1 } // ↑の2行を手短に書くとこう。やってることは同じ
5-3: () -> スーパータイプ
の関数オブジェクトを返す関数で、() -> サブタイプ
の関数オブジェクトを返せる
(類型3の亜種とも考えられる)
// 【Animal - Dog】パターン
func returnClosure() -> () -> Animal {
return { Dog() } // `() -> Animal`の関数オブジェクトを返す関数で、`() -> Dog`の関数オブジェクトを返せている
}
// 【T? - T】パターン
func returnClosure() -> () -> Int? {
return { 1 } // `() -> Int?`の関数オブジェクトを返す関数で、`() -> Int`の関数オブジェクトを返せている
}
この他にもできることがあれば、ご教示いただけると嬉しいです。
おわりに
さまざまなflatMap
の実体に、そのソースコードを読むことで迫ってきました。
もしもこの先、flatMap
が一見不可解な値を返してきた場面に遭遇したとしても、
ソースコードという「ここに立ち返れば大丈夫!」という道しるべを手にできたのは私にとって大きかったです。
そしてそれは、心の平穏ももたらしてくれるはずです。
一行一行、論理の整合性を踏みしめながら進んでいきたい私のような俗物にとって、型にうるさめのSwiftは本当にぴったりな言語だと感じています。
「この挙動はなぜ?」という事象に出くわしても、これまで集めてきた知識を組み合わせ、粘り強く考えていくことで解答にたどり着ける確率が非常に高いのは、Swiftの抗いがたい魅力です。
Array
のflatMap
の一部がcompactMap
へと切り分けられ、ジェネリクスの機能も漸進していく中、残る関心事は・・・flatMap
がプロトコルMonad
で抽象されたり、そのために必要な記述力をSwiftが獲得するーそんな未来はいつの日か訪れるのでしょうか?
・・・それに思いを馳せるには、自分自身さらなる精進が必要なので、今日はこれにて。
links
-
なお、
Array#map
に関してはこの記事では取り扱いません。思いのほか長尺になってしまったもののOptional#map
についてこれまで言及してきたのも、元を辿ればOptional#flatMap
をより理解したいと思ったが故でした。決してArray#map
の実装の意図を把握しきれなかったので取り上げることを放棄したといった話ではないと思います。それはさておいても、やはりArray
の実装についても徐々に知識を深めていかないとなと考えています。 ↩ -
この
flatMap
の使用例はソースコード内の文書化コメントからただパクってきたものなのですが、この記事を書いている際、ふとそのヘルプメッセージ(コメント)が古いまま残っていたことに気づきました。「なぜflatMap
ともあろうメジャーなAPIのコメントが2年前から放置されているんや...」と訝しみながらもPRを出してみたところ、12時間ほどでマージされてしまい(ただのコメント修正だからここまで短かったに違いない)、図らずとも自身初めてのSwiftへのコミット体験となりました。また、どう見積もっても書くのに1分とかからなかった2行の修正コードが、結果としてこれまで書いてきた中で最もインパクトを持つコードになりました。Contributionなんて言うのも憚れるミクロな修正でしたが、当の本人にとってはありえん嬉しみが深かったです。世界で一番好きな技術のコミュニティに受容される、これほど自己承認欲が充足され、また幸せなことは他にありません。この気持ちを忘れずに、次こそはコアライブラリ、そして標準ライブラリ・・・と手を入れられる開発者になれるよう、成長していきたいと思います。 ↩