Introduce Sequence.compactMap(_:)
01 何が問題だったのか
Swift 4.0 の時点で、Sequence には次の3種類の flatMap オーバーロードが存在していました。
Sequence.flatMap<S>(_: (Element) -> S) -> [S.Element]
where S : Sequence
Optional.flatMap<U>(_: (Wrapped) -> U?) -> U?
Sequence.flatMap<U>(_: (Element) -> U?) -> [U]
このうち3番目、「クロージャが Optional を返し、nil を除いた結果の配列を作る」オーバーロードが問題とされました。名前が flatMap であることから、シーケンスを平坦化するための操作だと読めてしまうのですが、実際にやっているのは「変換しつつ nil を除外する」ことであり、同じ名前の他のオーバーロードとは役割が大きく異なります。
さらに、Optional への暗黙的な昇格(implicit promotion)と組み合わさると、意図せずこのオーバーロードが選ばれてしまうケースが頻発しました。
struct Person {
var age: Int
var name: String
}
func getAges(people: [Person]) -> [Int] {
return people.flatMap { $0.age }
}
このコードでは、クロージャが返す Int が暗黙的に Int? に昇格され、flatMap の中で .some に包まれてすぐに剥がされる、という無駄な処理が走ります。本来ここで使うべきなのは単なる map です。
また Swift 4 で String が Collection に適合したことで、次のコードはそれまで3番目のオーバーロードで通っていたのに、より適した「シーケンスを返す flatMap」が選ばれるようになり、挙動が変わってしまいました。
func getNames(people: [Person]) -> [String] {
return people.flatMap { $0.name }
}
このように、名前と実際の動作が噛み合わないオーバーロードが存在することで、誤用が起きやすく、コンパイラのエラーメッセージも分かりにくくなっていました。
02 どのように解決されるのか
問題の flatMap オーバーロード(クロージャが Optional を返すもの)を非推奨にし、同じ機能を新しい名前 compactMap(_:) として導入します。「意図が最もよく伝わる名前」としてこの名前が選ばれました。
struct Person {
var age: Int
var name: String
}
let people: [Person] = [
Person(age: 20, name: "Alice"),
Person(age: 30, name: "Bob"),
]
// 変換しつつ nil を除外する
let names: [String] = people.compactMap { person in
person.name.isEmpty ? nil : person.name
}
シグネチャは元の flatMap と同じで、クロージャが Optional を返し、nil の要素を取り除いた配列を返します。
extension Sequence {
func compactMap<ElementOfResult>(
_ transform: (Element) throws -> ElementOfResult?
) rethrows -> [ElementOfResult]
}
従来の flatMap(Optional を返すクロージャを取るもの)は非推奨となり、コンパイラは警告と fix-it を出して compactMap への置き換えを促します。既存コードはそのままコンパイルできるため、段階的に移行できます。
// どちらもコンパイルは通るが、前者は非推奨
let ages1 = people.flatMap { $0.age > 25 ? $0.age : nil } // 警告 + fix-it
let ages2 = people.compactMap { $0.age > 25 ? $0.age : nil } // 推奨
Sequence を返すクロージャを取る flatMap と、Optional に対する flatMap はこれまで通り残ります。影響を受けるのは「Optional を返すクロージャを Sequence に渡す」オーバーロードだけです。
他言語での類似関数
同じ機能は他の言語にも存在し、それぞれ異なる名前が付いています。compactMap という命名は、nil を除外する意味を表す「compact」と、変換を表す「map」を組み合わせたもので、Swift の命名慣習に沿う選択として採用されました。
| 言語 | 名前 |
|---|---|
| Haskell / Idris | mapMaybe |
| OCaml | filter_map |
| F# | List.choose |
| Rust | filter_map |
| Scala | collect |
compact() について
「Optional のシーケンスから nil を取り除くだけ」の Sequence.compact() も有用ですが、これは「要素が Optional のときだけ使える」ことを現在の Swift の型システムでは表現できないため、本提案には含まれていません。当面は xs.compactMap { $0 } で代用します。