次のような 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 を使ってパースすることを想定しています。 ↩