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?

2
  • How do you expect "a" to map to an Int? Commented Aug 6, 2014 at 15:18
  • @Zaph "a".toInt() returns nil, so I want it filtered out of my array. Commented Aug 6, 2014 at 15:23

5 Answers 5

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"]
Sign up to request clarification or add additional context in comments.

Comments

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.

2 Comments

It's a bit confusing when you use 'a' as both a parameter name and a variable name.
looks to me like a new array is created for every appended item, with all the copying overhead .. O(n^2)
2

update: Xcode 7.2 • Swift 2.1.1

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

print(numbersOnly)   // [0,1]

Comments

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]

Comments

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
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.