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.
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
}
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:
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.
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:
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.
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.
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.
Fun fact: this one isn't lazy either! 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.
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.