Swift Digest
SE-0187 | Swift Evolution

Introduce Sequence.compactMap(_:)

Proposal
SE-0187
Authors
Max Moiseev
Review Manager
John McCall
Status
Implemented (Swift 4.1)

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 で StringCollection に適合したことで、次のコードはそれまで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]
}

従来の flatMapOptional を返すクロージャを取るもの)は非推奨となり、コンパイラは警告と 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 } で代用します。