24

I'm a little confused around flatMap (added to Swift 1.2)

Say I have an array of some optional type e.g.

let possibles:[Int?] = [nil, 1, 2, 3, nil, nil, 4, 5]

In Swift 1.1 I'd do a filter followed by a map like this:

let filtermap = possibles.filter({ return $0 != nil }).map({ return $0! })
// filtermap = [1, 2, 3, 4, 5]

I've been trying to do this using flatMap a couple ways:

var flatmap1 = possibles.flatMap({
    return $0 == nil ? [] : [$0!]
})

and

var flatmap2:[Int] = possibles.flatMap({
    if let exercise = $0 { return [exercise] }
    return []
})

I prefer the last approach (because I don't have to do a forced unwrap $0!... I'm terrified for these and avoid them at all costs) except that I need to specify the Array type.

Is there an alternative away that figures out the type by context, but doesn't have the forced unwrap?

MathewS
  • 2,267
  • 2
  • 20
  • 31
  • You probably meant Swift 1.2 vs 1.1, there is no Swift 1.3 yet (or did I miss something?) – Martin R Apr 25 '15 at 20:47
  • Opps, yes think I had Xcode 6.3 on my mind... updated question - thanks! – MathewS Apr 25 '15 at 20:52
  • 1
    Although you *can* use `flatMap { $0 }` to remove nils, the real question is *should* you. If you're not careful, `flatMap` can lead to bugs, so I recommend using [removeNils](http://stackoverflow.com/a/38548106/35690) instead. – Senseful Jul 24 '16 at 02:20

5 Answers5

40

Since Swift 4.1 you can use compactMap:

let possibles:[Int?] = [nil, 1, 2, 3, nil, nil, 4, 5]
let actuals = possibles.compactMap { $0 }

(Swift 4.1 replaced some overloads of flatMap with compactmap. If you are interested in more detail on this then see for example: https://useyourloaf.com/blog/replacing-flatmap-with-compactmap/ )

With Swift 2 b1, you can simply do

let possibles:[Int?] = [nil, 1, 2, 3, nil, nil, 4, 5]
let actuals = possibles.flatMap { $0 }

For earlier versions, you can shim this with the following extension:

extension Array {
    func flatMap<U>(transform: Element -> U?) -> [U] {
        var result = [U]()
        result.reserveCapacity(self.count)
        for item in map(transform) {
            if let item = item {
                result.append(item)
            }
        }
        return result
    }
}

One caveat (which is also true for Swift 2) is that you might need to explicitly type the return value of the transform:

let actuals = ["a", "1"].flatMap { str -> Int? in
    if let int = str.toInt() {
        return int
    } else {
        return nil
    }
}
assert(actuals == [1])

For more info, see http://airspeedvelocity.net/2015/07/23/changes-to-the-swift-standard-library-in-2-0-betas-2-5/

Cortado-J
  • 2,035
  • 2
  • 20
  • 32
Fizker
  • 2,434
  • 1
  • 18
  • 12
  • I saw flatMap but didn't know you could use it to filter optionals like that. Really nice addition to Swift 2! – MathewS Jul 29 '15 at 01:31
15

I still like the first solution, which creates only one intermediate array. It can slightly more compact be written as

let filtermap = possibles.filter({ $0 != nil }).map({ $0! })

But flatMap() without type annotation and without forced unwrapping is possible:

var flatmap3 = possibles.flatMap {
    flatMap($0, { [$0] }) ?? []
}

The outer flatMap is the array method

func flatMap<U>(transform: @noescape (T) -> [U]) -> [U]

and the inner flatMap is the function

func flatMap<T, U>(x: T?, f: @noescape (T) -> U?) -> U?

Here is a simple performance comparison (compiled in Release mode). It shows that the first method is faster, approximately by a factor of 10:

let count = 1000000
let possibles : [Int?] = map(0 ..< count) { $0 % 2 == 0 ? $0 : nil }

let s1 = NSDate()
let result1 = possibles.filter({ $0 != nil }).map({ $0! })
let e1 = NSDate()
println(e1.timeIntervalSinceDate(s1))
// 0.0169369578361511

let s2 = NSDate()
var result2 = possibles.flatMap {
    flatMap($0, { [$0] }) ?? []
}
let e2 = NSDate()
println(e2.timeIntervalSinceDate(s2))
// 0.117663979530334
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Thanks! Given that the filter/map option is faster I might stick with it since (for me) it's clearest what's happening. – MathewS Apr 25 '15 at 23:38
  • in swift 2 you can also simply go `possibles.flatmap{ $0 }`. See docs: `public func flatMap(@noescape transform: (Self.Generator.Element) throws -> T?) rethrows -> [T]` Flatmap returns an `Array` containing the non-nil results of mapping `transform` over `self`. – Daniel Galasko Nov 25 '15 at 14:40
  • @DanielGalasko: Yes, that's what Fizker writes in the (now) accepted answer. I think that wasn't available when I wrote this answer. – Martin R Nov 25 '15 at 14:49
2

Related to the question. If you are applying flatMap to an optional array, do not forget to optionally or force unwrap your array otherwise it will call flatMap on Optional and not objects conforming to Sequence protocol. I made that mistake once, E.g. when you want to remove empty strings:

var texts: [String]? = ["one", "two", "", "three"] // has unwanted empty string

let notFlatMapped = texts.flatMap({ $0.count > 0 ? $0 : nil })
// ["one", "two", "", "three"], not what we want - calls flatMap on Optional

let flatMapped = texts?.flatMap({ $0.count > 0 ? $0 : nil })
// ["one", "two", "three"], that's what we want, calls flatMap on Array
Au Ris
  • 4,541
  • 2
  • 26
  • 53
0

You could use reduce:

let flattened = possibles.reduce([Int]()) { 
        if let x = $1 { return $0 + [x] } else { return $0 } 
    }

You are still kind of declaring the type, but it's slightly less obtrusive.

letvargo
  • 136
  • 4
  • That's clever use of reduce, but (for me) takes too much mental unwrapping to see what's happening. Thanks for the idea! – MathewS Apr 25 '15 at 23:11
0

Since this is something I seem to end up doing quite a lot I'm exploring a generic function to do this.

I tried to add an extension to Array so I could do something like possibles.unwraped but couldn't figure out how to make an extension on an Array. Instead used a custom operator -- hardest part here was trying to figure out which operator to choose. In the end I chose >! to show that the array is being filtered > and then unwrapped !.

let possibles:[Int?] = [nil, 1, 2, 3, nil, nil, 4, 5]

postfix operator >! {}

postfix func >! <T>(array: Array<T?>) -> Array<T> {
    return array.filter({ $0 != nil }).map({ $0! })
}

possibles>!
// [1, 2, 3, 4, 5]
MathewS
  • 2,267
  • 2
  • 20
  • 31