この記事は 2018/4/14に行われた Swift Tweets 2018 Spring で私 @amarillons が発表した内容と同じです。https://togetter.com/li/1218153
Swift 4.1で導入された compactMap
について、その内容と背景を紹介します。
これまでは、2種類の flatMap
がオーバーロードされていました。
Sequence.flatMap<S>(_: (Element) -> S) -> [S.Element]
where S : Sequence // (A)
Sequence.flatMap<U>(_: (Element) -> U?) -> [U] // (B)
まずは二つのはたらきについて簡単に説明します。(A)は普通の flatMap
です。map
に似ていますが、2重にネストした Sequence
が1重に展開されます。
[1, 2, 3].map { [Int](repeating: $0, count: $0) } // [[1], [2, 2], [3, 3, 3]]
[1, 2, 3].flatMap { [Int](repeating: $0, count: $0) } // [1, 2, 2, 3, 3, 3]
(B)は特殊な flatMap
です。これの主な使い道の一つは Sequence
から nil
を取り除くことです。
[1, 2, nil, 4, 5].flatMap { $0 } // [1, 2, 4, 5]
["1", "2", "three", "4", "5"]
のような Array
を、各要素を整数に変換して [1, 2, 4, 5]
を取得したいようなときにも使えます。
["1", "2", "three", "4", "5"].flatMap { Int($0) } // [1, 2, 4, 5]
flatMap
ではなく map
でこれをやろうとしても、 String
から Int
への変換は Int("1")!
のように行えますが、 Int("three")!
では変換に失敗してクラッシュしてしまいます。
["1", "2", "three", "4", "5"].map { Int($0)! } // クラッシュ
map
と filter
を組み合わせて書くこともできますが、あまりスマートではありません。!が出てくるのも避けたいです。(B)の flatMap
を使えば先程見たように簡単に書けます。
["1", "2", "three", "4", "5"]
.map { Int($0) }
.filter { $0 != nil }
.map { $0! } // [1, 2, 4, 5]
(B)の flatMap
が便利なのはわかりましたが、(A)と(B)は一見関係ない処理に見えます。どうしてどちらも flatMap
という名前だったのでしょうか。
その理由は、 Optional
を要素が0個か1個の Sequence
だと考えれば(B)は(A)と同じ処理になるからです。
[[1], [], [3]].flatMap { $0 } // (A) [1, 3]
[.some(1), .none, .some(3)].flatMap { $0 } // (B) [1, 3]
Swift 4.1で実装されたswift-evolutionのSE-0187 https://github.com/apple/swift-evolution/blob/master/proposals/0187-introduce-filtermap.md では、(B)の flatMap
を compactMap
にリネームするという変更がなされました。
これは、(B)の flatMap
には問題があったからです。Proposalに挙げられている例を使って説明します。
次のような Person
型を考えます。
struct Person {
var age: Int // 年齢
var name: String // 名前
}
Person
の Array
である people
から、年齢の Array
を作る操作をしたいとします。
let people: [Person] = ...
let ages: [Int] = ???
これは map
を使って簡単にできます。
let people: [Person] = ...
let ages: [Int] = people.map { $0.age }
しかし、 flatMap
を使って書いても特にエラーにはなりません。
let people: [Person] = ...
let ages = people.flatMap { $0.age }
エラーにはなりませんが、ここでは無駄なラッピング・アンラッピングが行われています。先程のコードは、実際には次のコードと同じです。
let people: [Person] = ...
let ages = people.flatMap { .some($0.age) }
{ $0.age }
の age
は Sequence
ではないので、 (Element)->U?
をとる(B)の flatMap
を使おうとしていると解釈されます。
したがって、 $0.age
は .some
にラップされてしまいます。
しかし age
はもともと Int
で決して nil
にならないので、結局すべての要素が残されることになり、このラッピングは無駄な操作になります。
このような処理では flatMap
ではなく map
を使うべきですが、 flatMap
を使ってもコンパイルエラーにならないため、誤った使い方をしていることに気づかないことになります。これが(B)の flatMap
の問題です。
Proposalでは別のケースにも言及されています。 people
を age
の Array
に変換する代わりに name
の Array
に変換する場合を考えてみます。結果が [Int]
になるか [String]
になるかだけの違いです。
map
と flatMap
を間違えた場合、Swift 3では age
のときと同じように(B)の flatMap
が使われて [String]
が得られていました。
let people: [Person] = ...
let names = people.flatMap { $0.name } // (B)が使われてnamesは[String]
しかし、Swift 4では String
が Sequence
に適合したため、暗黙の型変換をともなわない(A)の flatMap
と解釈されてしまいます。
let people: [Person] = ...
let names = people.flatMap { $0.name } // (A)が使われてnamesは[Character]
結果的に、コードを書いた時は(B)の flatMap
を適用しようとしていたのに、(A)の flatMap
が適用されてしまうことになります。
names
の型が明記されていればコンパイルエラーとなって気付くことができますが、型推論したままJSONに書き出していたりすると問題が起こるまで気付くことができません。
let people: [Person] = ...
let names = people.flatMap { $0.name } // (A)か(B)でnamesの型が変わる
let jsonData = try JSONEncoder().encode(names) // (A)でも(B)でもOK
このような問題が起こるのは Optional
への暗黙の型変換が行われていることが原因です。しかし、暗黙の型変換をなくすのは現実的でないので、SE-0187では(B)の flatMap
を compactMap
にリネームすることによりオーバーロードを解消しようとしています。
もう一点、Proposalで言及されている compact
メソッドについても紹介します。 Sequence
に含まれる nil
を取り除きたいときは、今回導入された compactMap
を使って次のように書けます。
// nilを取り除いて[Int?]から[Int]に変換
let optionalArray: [Int?] = ...
let nonOptionalArray: [Int] = optionalArray.compactMap { $0 }
これはよく使う操作なので、もし compact
というメソッドがあって optionalArray.compact()
と書ければより便利になると考えられます。しかし、Swift 4.1ではそのような compact
メソッドを実装することはできません。
実装にはGenerics Manifestoに書かれているParameterized Extensions https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md#parameterized-extensions が必要になります。
extension<U> Sequence where Element == Optional<U> {
func compact() -> [U] {
return compactMap { $0 }
}
}
将来的には compact
も標準ライブラリに追加されることが期待されます。
以上、Swift 4.1で導入された compactMap
について紹介しました。