user1002430
user1002430

Reputation:

Swift function to find first element of collection matching a predicate?

If xs is a collection and pred is a closure that returns a Bool, is there a built-in function that does the following?

xs.filter(pred).first

This gets the first element of a collection matching the predict, or nil if there is no match. Not interested in the index, but the element itself.

Upvotes: 2

Views: 2781

Answers (2)

Imanou Petit
Imanou Petit

Reputation: 92549

In the simplest case, what you want may look like this:

let array = [18, 12, 35, 11, 12, 44]
var first: Int?

for element in array where element == 12 {
    first = element
    break
}

print(first) // prints: Optional(12)

If you really need to set a predicate closure, you can use the following pattern:

let array = [18, 12, 35, 11, 12, 44]
var first: Int?

let predicateClosure = { (value: Int) -> Bool in
    return value == 12
}

for element in array where predicateClosure(element) {
    first = element
    break
}

print(first) // prints: Optional(12)

If you need to repeat these operations, you can refactor your code by using a SequenceType protocol extension:

extension SequenceType where Generator.Element == Int {
    func getFirstWithPredicate(predicate: Int -> Bool) -> Int? {
        for element in self where predicate(element) {
            return element
        }
        return nil
    }
}

let array = [18, 12, 35, 11, 12, 44]
let predicateClosure: Int -> Bool = {
    return $0 == 12
}

let first = array.getFirstWithPredicate(predicateClosure)
print(first) // prints: Optional(12)

Note that you don't need your predicate closure parameter to escape your getFirstWithPredicate(_:) method here, so you can add the @noescape attribute before it (see Nonescaping Closures for more details):

extension SequenceType where Generator.Element == Int {
    func getFirstWithPredicate(@noescape predicate: Int -> Bool) -> Int? {
        for element in self where predicate(element) {
            return element
        }
        return nil
    }
}

let array = [18, 12, 35, 11, 12, 44]
let predicateClosure = { $0 == 12 }

let first = array.getFirstWithPredicate(predicateClosure)
print(first) // prints: Optional(12)

If you want the previous code to work for any type of sequence, you can remove the Int constraint and redeclare your getFirstWithPredicate(_:) method like in the following example:

extension SequenceType {
    func getFirstWithPredicate(@noescape predicate: Generator.Element -> Bool) -> Generator.Element? {
        for element in self where predicate(element) {
            return element
        }
        return nil
    }
}

let intArray = [18, 12, 35, 11, 12, 44]
let firstInt = intArray.getFirstWithPredicate { $0 == 12 }
print(firstInt) // prints: Optional(12)

let stringArray = ["Car", "Boat", "Plane", "Boat", "Bike"]
let firstString = stringArray.getFirstWithPredicate { $0 == "Boat" }
print(firstString) // prints: Optional("Boat")

If you're using a CollectionType protocol conforming object (for example, an Array), you can easily get the last element that matches your predicate with the same getFirstWithPredicate(_:) declaration by using reverse():

extension SequenceType {
    func getFirstWithPredicate(@noescape predicate: Generator.Element -> Bool) -> Generator.Element? {
        for element in self where predicate(element) {
            return element
        }
        return nil
    }
}

struct Toy {
    let name: String
    let price: Int
}

let array = [
    Toy(name: "Ball", price: 20),
    Toy(name: "Car", price: 12),
    Toy(name: "Plane", price: 35),
    Toy(name: "Boat", price: 12),
]

let lastToyWithMatchingPrice = array.reverse().getFirstWithPredicate { $0.price == 12 }
print(lastToyWithMatchingPrice) // prints: Optional(Toy(name: "Boat", price: 12))

Upvotes: 1

Kametrixom
Kametrixom

Reputation: 14983

No there isn't, but you can write one yourself like this:

extension SequenceType {
    func first(@noescape pred: Generator.Element throws -> Bool) rethrows -> Generator.Element? {
        return try filter(pred).first
    }
}

EDIT: This version isn't optimal, since the filter creates a whole new array, even though only the first element would be needed. As noted by Martin R, lazy.filter also doesn't work for. This would be necessary to make it work with lazy:

extension CollectionType {
    func first(pred: Generator.Element -> Bool) -> Generator.Element? {
        return lazy.filter(pred).first
    }
}

Because:

  • @noescape can't be used because @noescape means that the closure cannot escape the current function, which would be possible when passing it to the lazy filter (doesn't evaluate elements until it is asked to -> has to escape the predicate)
  • throws can't be used because the filter is lazy -> errors wouldn't be thrown immediately, but rather when it is used the first time which would be when calling first, but first can't be throwing because it's a property. There are some discussions on making getters (and subscripts) throwing in future versions of Swift.
  • CollectionType has to be used, because only LazyCollectionType has the first property.

So to really make it lazy and have all the @noescape, throws and SequenceType, you'd have to use an imperative approach:

extension SequenceType {
    func first(@noescape pred: Generator.Element throws -> Bool) rethrows -> Generator.Element? {
        for elem in self where try pred(elem) {
            return elem
        }
        return nil
    }
}

Upvotes: 2

Related Questions