gohnjanotis
gohnjanotis

Reputation: 7575

Are there cases where firstIndex doesn't return an optional in Swift?

I have a simple function to find a specific element by index in an array using firstIndex like this, where Entry.id is a String:

private func findUserEntry(inEntries entries: [Entry],
                           currentUserID: String?) -> Entry? {
    
    guard let currentUserID = currentUserID else { return nil }
     
    guard let index = entries.firstIndex(where: { $0.id == currentUserID }) else { return nil }

    return entries[index]
}

I was refactoring my code to try to accomplish the same thing in different ways, as a way to learn and practice. I thought this function would effectively do the same thing:

private func findUserEntry(inEntries entries: [Entry],
                           currentUserID: String?) -> Entry? {
    
    guard let currentUserID = currentUserID else { return nil }
    
    return entries
        .firstIndex { $0.id == currentUserID }
        .map { possibleIndex -> Entry? in
            
            guard let index = possibleIndex else { return nil }
            
            return entries[index]
        }
}

However, I'm running into an error: Initializer for conditional binding must have Optional type, not 'Array<Entry>.Index' (aka 'Int'). So, somehow, the value of possibleIndex coming out of .firstIndex into .map is an Int instead of the Int? I expected, like it is in the original version above. Why is there a difference here?

Upvotes: 0

Views: 1322

Answers (3)

Duncan C
Duncan C

Reputation: 131436

If you look up the Array function firstIndex(where:) in Apple's docs you'll find this: https://developer.apple.com/documentation/swift/array/2994722-firstindex

The function is declared to return an Optional Integer:

func firstIndex(where predicate: (Element) throws -> Bool) rethrows -> Int?

So no, it will ALWAYS return an Optional.

However, you are using map, which in this case is a function on Optional:

https://developer.apple.com/documentation/swift/optional/1539476-map

The function Optional.map() is declared like this:

func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?

If the Optional contains a value, it is unwrapped and passed to the closure of your map statement. If the Optional contains nil, the map function returns nil.

Thus in your code

private func findUserEntry(inEntries entries: [Entry],
                           currentUserID: String?) -> Entry? {
    
    guard let currentUserID = currentUserID else { return nil }
    
    return entries
        .firstIndex { $0.id == currentUserID }
        .map { possibleIndex in 
            //At this point the value in possibleIndex can't be nil, 
            //so there is no need for the guard statement.
            //guard let index = possibleIndex else { return nil }
            return entries[possibleIndex]
        }
}

There is no need for your guard statement. If firstIndex contains nil, the map statement returns nil. If firstIndex contains a value, the map statement passes the unwrapped value in firstIndex to the closure and returns the result of that closure.

Upvotes: 1

user652038
user652038

Reputation:

Other people answered your question, but what you really want is flatMap.

currentUserID.flatMap { currentUserID in
  entries.first { $0.id == currentUserID }
}

Or, if Entry.id is not optional, then remove the flat-mapping.

Upvotes: 1

New Dev
New Dev

Reputation: 49590

Array<Int>.firstIndex returns an optional Array<Int>.Index, which is an Int?.

So, .map(_:) that you're using is a method of Optional<Int> with the following signature:

func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?

Evaluates the given closure when this Optional instance is not nil, passing the unwrapped value as a parameter.

Upvotes: 3

Related Questions