次のような String の Array があったとき、各要素を Int に変換するにはどうすれば良いでしょうか?ただし、 Int に変換できない要素は結果から除くものとします。
let strings = ["2", "3", "four", "5"]
let numbers = ... // 結果は [2, 3, 5]
for ループで回すとかは面倒すぎるのでなしです。
reduce
最初に思いつきそうな方法は、万能の高階関数 reduce を使う方法です。
let numbers = strings.reduce([Int]()) { (var numbers, string) in
if let number = Int(string) {
numbers.append(number)
}
return numbers
}
なんか、 for ループを回すのと変わらない面倒さです・・・。
map & filter
パースできないものは取り除いて変換したいんだから、 map と filter を組み合わせれば良さそうです。
let numbers = strings.map { Int($0) }
.filter { $0 != nil }
とてもシンプルになりました。
しかし、ちょっと待って下さい!!これでは numbers の型が [Int] ではなく [Int?] になってしまいます。仕方ないので型の変換を行う map を付け足しましょう。
let numbers = strings.map { Int($0) }
.filter { $0 != nil }
.map { $0! }
ずいぶんと不格好になってしまいました・・・。しかも nil のチェックと ! が二重に入るのは許せません。
flatMap (Swift 1.2)
そんなときに役に立つのが flatMap でした。変換に失敗した nil を空の Array に置き換えることで、その要素を消すことができるのです。
let numbers = strings.flatMap { Int($0) ?? [] } // コンパイルエラー
おっと、これでは Int($0) で変換に成功したときが Array になっていないので flatMap できません。少し不格好ですが次のようにして、成功した場合も [Int] に変換しましょう。
let numbers = strings.flatMap { Int($0).map { [$0] } ?? [] }
気をつけてほしいのは、この map は Array ではなく Optional の map だということです。
.map { [$0] } ?? [] でやっていることは、 Optional が値を持っていれば 1 要素の Array に、 nil であれば空の Array に変換するということです。これは、 "SwiftのOptional型を極める" で書いたように Optional を要素が 1 個か 0 個の Array と考えることができるという話とリンクしています。 Optional を Array に見立てることで flatMap で flat にできるわけです。
このパターンは頻出で、型を変換しながら変換に失敗した要素は取り除きたいという場合に必須のイディオムでした。しかし、 Optional を Array に変換するためにわざわざ .map { [$0] } ?? [] と書くのは面倒です。
flatMap (Swift 2.0)
これを解決してくれるのが Swift 2.0 で導入された新しい flatMap です。新しい flatMap を使えば次のように書けます。
let numbers = strings.flatMap { Int($0) }
めちゃくちゃシンプルですね!!
この flatMap の宣言は次のようになっています(リファレンス)。
// protocol SequenceType
func flatMap<T>(@noescape _ transform: (Self.Generator.Element) -> T?) -> [T]
つまり、 Optional を 1 要素か 0 要素の Array と見なすという話を、 flatMap のオーバーロードで実現 してくれているわけです。
これで型の変換がはかどりますね!
より現実的なケース
Item というクラスがあり、そのオブジェクトが JSON ファイルで保存されているとします。今、複数の JSON ファイルのパスを [String] で保持しているとして、それらの指す JSON ファイルをパースして [Item] を読み込むケースを考えてみましょう。
おそらく、次のような処理手順を思い浮かべるはずです1。
-
pathのStringからNSDataを読み込む。 -
NSDataからJSONオブジェクトを生成する。 -
JSONをパースしてItemオブジェクトを生成する。
上記は一つのファイルから一つの Item オブジェクトを生成する処理ですが、 map と flatMap を使えば、この処理手順をそのまま複数の( Array の)処理として書くことができます。
return paths.flatMap { NSData(contentsOfFile: $0) } // [String] -> [NSData]
.map { JSON(data: $0) } // [NSData] -> [JSON]
.flatMap { Item.fromJSON($0) } // [JSON] -> [Item]
1 行目はパスの [String] を [NSData] に変換していますが、 NSData(contentsOfFile:) は Failable initializer なので戻り値は NSData? です。もし map を使っていたら戻り値の型は [NSData?] になってしまいますが、 flatMap を使って flat にすることで [NSData] を得ることができます。これは、 Optional を Array とみなして、 [[NSData]] が flat にされて [NSData] になったとも考えられます。
2 行目の JSON の Initializer は失敗しないので、ただの map で十分です。
3 行目の Item.formJSON() はパースに失敗する可能性があるので戻り値の型は Item? です。そのため、ここでも 1 行目同様に flatMap が必要になります。
このように、途中で失敗する可能性があるような処理でも、新しい flatMap を使うことで簡単に扱うことができるようになります。
単数の場合
余談ですが、単数の場合でも Optional の map と flatMap を使って同じように書くことができます。これは、 Optional を要素数 0 か 1 の Array と見なすことができるという話からも当然のことです。
return NSData(contentsOfFile: path) // String -> NSData?
.map { JSON(data: $0) } // NSData? -> JSON?
.flatMap { Item.fromJSON($0) } // JSON? -> Item?
2 行目は失敗する可能性がないので map 、 3 行目は失敗する可能性があるので flatMap となります。
Swift 2.0 の新しい flatMap を Swift 1.2 で実装してみる
同じものを Extension で作ってやれば Swift 1.2 でも使えるようになるので swiftf に実装してみました。
extension Array {
func flatMap<U>(@noescape transform: T -> U?) -> [U] {
return flatMap { transform($0).map { [$0] } ?? [] }
}
}
let strings: [String] = ["2", "3", "four", "5"]
let numbers = strings.flatMap { $0.toInt() } // [2, 3, 5]
-
JSON は SwiftyJSON を使ってパースすることを想定しています。 ↩