Adding firstAs() to Sequence

Hey SE!

I often find it necessary (and I imagine this is somewhat common) to find and cast the first matching element of a sequence. There are of course a few techniques that could be used to do this. For example:

sequence.filter { $0 is MyType }.first as? MyType
sequence.compactMap { $0 as? MyType }.first
sequence.first { $0 is MyLongTypeName } as? MyLongTypeName
if let element = sequence.first(where: { $0 is MyLongTypeName }) as? MyLongTypeName {
    // do something
}

The last option should leave us with the fewest iterations and is the option I've chosen to implement as a simple helper in a number of my projects:

extension Sequence {
    public func firstAs<T>(_ type: T.Type = T.self) -> T? {
        first { $0 is T } as? T
    }
}

This can allow for very compact, straightforward, and efficient calls to find the first element you're looking for:

// explicit type
if let element = sequence.firstAs(MyType.self) {
    // do something
}
// inferred type
if let element: MyLongTypeName = sequence.firstAs() {
    // do something
}
// inferred type
let label: UILabel?

switch searchArea {
case .subviews:
    label = subviews.firstAs()
case .arrangedSubviews:
    label = stackView.arrangedSubviews.firstAs()
}

I wanted to put out some feelers here in the forum to see how the community felt about an official proposal for this simple helper.

Thanks!

  • Dan
5 Likes

I think the proposed spelling makes it easy to misunderstand, ie it could mean the same as this:

if let element = sequence.first as? MyType {
   ...
}

And perhaps something like the following more general method could be used for this as well as other use cases:

extension Sequence {

  /// Returns the first non-`nil` result of the given transformation.
  public func first<R>(
    _ transform: (Element) throws -> R?
  ) rethrows -> R? {
    for element in self {
      if let result = try transform(element) { return result }
    }
    return nil
  }

}

if let element = sequence.first({ $0 as? MyType }) {
  // do something
}
1 Like

I don’t have strong opinions on whether the proposed extension is worth adding, but I think it’s worth noting that you should be able to get the performance of first(where:) more idiomatically by using lazy:

sequence.lazy.compactMap { $0 as? MyType }.first
12 Likes

I admit I only skimmed the first post and just assumed this is what it meant. Then I read your post and found out this is actually not what it means and I was very confused. So chalk me up as someone who immediately misunderstood the proposed spelling.

I do think the proposed feature would be useful, but I’m not sold on the idea that it needs to be in stdlib.

3 Likes

I find myself doing this occasionally too, although I call this firstMap:

extension Sequence {
    func firstMap<T>(where mapper: (Element) -> T?) -> T?
}

This lets me do:

let firstConcrete = arrayOfExistentials.firstMap { $0 as? ConcreteType }
// firstConcrete is a "ConcreteType?"

As a "concrete" example, I'll use this in an MKMapViewDelegate callback when dealing with cluster annotations:

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    if annotation is MKUserLocation { return nil }
    
    if let appAnnotation = annotation as? AppAnnotation {
        return self.constructViewFromAppAnnotation(appAnnotation)
    }
    
    if let clusterAnnotation = annotation as? MKClusterAnnotation {
        let clusterMember = clusterAnnotation.memberAnnotations.firstMap { $0 as? AppAnnotation }
        return self.constructViewForCluster(clusterMember, clusterAnnotation)
    }
    
    return nil
}
2 Likes

Isn't this all just equivalent to arrayOfExistentials.first.map { $0 as? ConcreteType }?

Not quite, as that only considers the element at the startIndex of the array, and not the first matching element anywhere within the array.

For example:

let heterogenousJSON: [Any] = [
    42,
    "Hello",
    ["name": "Arthur"]
]

let firstString = heterogenousJSON.firstMap { $0 as? String } 
// returns "Hello"

let firstElementAsString = heterogenousJSON.first.map { $0 as? String }
// returns nil, because it only considers 42

Well, the name firstMap certainly sounds like that's what it's doing. So @Torust's suggestion is the real equivalence.

Yeah, seeing those two side-by-side indicates that perhaps firstMap isn't the best name. I picked it because of its symmetry with first(where:) and compactMap(_:).

I believe this is worth adding to the Standard Library, because it meets The Law of Soroush:

If an extension meets any one of the following four criteria, it deserves to exist:

  1. Does it increase expressivity?
  2. Does it decrease noise?
  3. Does it include a performance optimization?
  4. Does it belong on every instance of the type?
1 Like

@Torust:
sequence.lazy.compactMap { $0 as? MyType }.first

This is also the implementation I use in my version, which I call firstOfType().

3 Likes

I would disagree on most of those points, given the equivalence expressed up thread. However, a more general solution to conditional mapping might be a good idea. It's difficult to express both ideas simultaneously, as the type casting and Bool checks will handle differently.

I'm fairly frequently using two extensions I've written:

  • compactCast(to:) which casts the array to a certain type. Example: self.subviews.compactCast(to: NSButton.self)
  • func firstNonNilValue<U>(using transformation: (Self.Element) -> U?) -> U?

These two are IMHO more universally usable than just firstAs... I'd suggest rather adding something in similar - like @Jens suggested.

1 Like

:+1: That's another one that I have under a slightly different name:

func firstResult<T>(_ predicate: (Element) -> T?) -> T?
{
  return lazy.compactMap(predicate).first
}
1 Like

To be honest I don’t think this proposal will be deemed worthy of being added to the standard library. Sometimes it may be useful to have such a method at hand, but I think that it’s use would be really limited.

I find the inferred type examples confusing. It should be possible to read the code out loud but your sentence ends in the middle. firstAs() what?

Fun fact! This version isn't actually lazy. LazySequenceProtocol.compactMap takes an escaping closure, which this predicate isn't, so the compactMap is silently inferred to be the regular Sequence.compactMap.

6 Likes

You’re right!

extension Sequence {
  func firstResult<T>(_ predicate: (Element) -> T?) -> T? {
    return withoutActuallyEscaping(predicate) { escapablePredicate in
      return self.lazy.compactMap(escapablePredicate).first
    }
  }
}
1 Like

OK that makes sense... thought when I command-ctrl click on compactMap in Xcode it shows me LazySequence.compactMap:

    @inlinable public func compactMap<ElementOfResult>(_ transform: (Base.Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]

...and there's no @escaping there. Is Xcode just showing me the wrong function?

The distinction between LazySequence and LazySequenceProtocol, and why we need both, is not obvious to me at all.

Fun fact: this one isn't lazy either! :sweat_smile: This time for a completely different reason...

The lazy overload of compactMap returns some LazySequenceProtocol value, and the eager version returns a [T]. Calling .first on this resolves this because Sequence doesn't have a first property — Collection does, and therefore the eager version that returns an array is picked.

Some ways to remedy this:

  • Replace .first with .first(where: { _ in true }), which is defined on Sequence ¯\_(ツ)_/¯
  • Extend Collection instead of Sequence because then the lazy overload of compactMap returns some LazyCollectionProtocol value which does have a .first property (from Collection).
  • Just use a good old for-loop. Probably the best idea given how finicky all of this is, and then you can also more easily make it rethrows.
7 Likes

That's the eager version from Sequence, not the lazy version. The lazy version has an @escaping closure, isn't marked as rethrows, and doesn't return an array.

2 Likes