6

Lets say I have an Array of String and I want to map it to an Array of Int I can use the map function:

var arrayOfStrings: Array = ["0", "a"]
let numbersOptional = arrayOfStrings.map { $0.toInt() }
// numbersOptional = "[Optional(0), nil]"

Numbers is now an Array of Int?, but I want an Array of Int. I know I can do this:

let numbers = arrayOfStrings.map { $0.toInt() }.filter { $0 != nil }.map { $0! }
// numbers = [0]

But that doesn't seem very swift. Converting from the Array of Int? to Array of Int requires calling both filter and map with pretty much boilerplate stuff. Is there a more swift way to do this?

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
Richard Venable
  • 8,310
  • 3
  • 49
  • 52

5 Answers5

6

Update: As of Swift 1.2, there's a built-in flatMap method for arrays, but it doesn't accept Optionals, so the helper below is still useful.


I like using a helper flatMap function for that sort of things, just like Scala's flatMap method on collections (which can consider an Scala Option as a collection of either 0 or 1 element, roughly spoken):

func flatMap<C : CollectionType, T>(source: C, transform: (C.Generator.Element) -> T?) -> [T] {
    var buffer = [T]()
    for elem in source {
        if let mappedElem = transform(elem) {
            buffer.append(mappedElem)
        }
    }
    return buffer
}

let a = ["0", "a", "42"]
let b0 = map(a, { $0.toInt() }) // [Int?] - [{Some 0}, nil, {Some 42}]
let b1 = flatMap(a, { $0.toInt() }) // [Int] - [0, 42]

This definition of flatMap is rather a special case for Optional of what a more general flatMap should do:

func flatMap<C : CollectionType, T : CollectionType>(source: C, transform: (C.Generator.Element) -> T) -> [T.Generator.Element] {
    var buffer = [T.Generator.Element]()
    for elem in source {
        buffer.extend(transform(elem))
    }
    return buffer
}

where we'd get

let b2 = flatMap(a, { [$0, $0, $0] }) // [String] - ["0", "0", "0", "a", "a", "a", "42", "42", "42"]
Jean-Philippe Pellet
  • 59,296
  • 21
  • 173
  • 234
5

Using reduce to build the new array might be more idiomatic

func filterInt(a: Array<String>) -> Array<Int> {
    return a.reduce(Array<Int>()) {
        var a = $0
        if let x = $1.toInt() {
            a.append(x)
        }
        return a
    }
}

Example

filterInt(["0", "a", "42"]) // [0, 42] 

What you would really want is a collect (map + filter) method. Given the specific filter you need to apply, in this case even a flatMap would work (see Jean-Philippe's answer). Too bad both methods are not provided by the swift standard library.

Gabriele Petronella
  • 106,943
  • 21
  • 217
  • 235
2

There’s no good builtin Swift standard library way to do this. However, Haskell has a function, mapMaybe, that does what you’re looking for (assuming you just want to ditch nil values). Here’s an equivalent in Swift:

func mapSome<S: SequenceType, D: ExtensibleCollectionType>
  (source: S, transform: (S.Generator.Element)->D.Generator.Element?)
  -> D {
        var result = D()
        for x in source {
            if let y = transform(x) {
                result.append(y)
            }
        }
        return result
}

// version that defaults to returning an array if unspecified
func mapSome<S: SequenceType, T>
  (source: S, transform: (S.Generator.Element)->T?) -> [T] {
    return mapSome(source, transform)
}

let s = ["1","2","elephant"]
mapSome(s) { $0.toInt() }  // [1,2]
Airspeed Velocity
  • 40,491
  • 8
  • 113
  • 118
2

update: Xcode 7.2 • Swift 2.1.1

let arrayOfStrings = ["0", "a", "1"]
let numbersOnly = arrayOfStrings.flatMap { Int($0) }

print(numbersOnly)   // [0,1]
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
1

You can consider using reduce, it's more flexible:

var arrayOfStrings: Array = ["0", "a"]
let numbersOptional = arrayOfStrings.reduce([Int]()) { acc, str in
  if let i = str.toInt() {
    return acc + [i]
  }

  return acc
}
cxa
  • 4,238
  • 26
  • 40