Adding firstAs() to Sequence

Are you saying that adding the .first on the end changes which version of compactMap gets called? I would have expected that kind of thing to be determined in more of a left-to-right fashion.

In my own code, that function is in an extension of Array because in practice I just don't have a need for putting it all the way down in Sequence. compactMap then returns (according to Xcode) a LazyMapSequence<LazyFilterSequence<LazyMapSequence<LazySequence<Array<Element>>.Elements, T?>>, T>. Let's see.. that's got four lazies in it. Is that enough? :)

Yes. It will default to the overloaded version if possible and only use the base version when needed, which is usually exactly what you want, but it can also cause surprises.

It is enough! The inner Array<Element> is a Collection, which makes the enclosing LazySequence a Collection due to conditional conformance, which makes the LazyFilterSequence a Collection in the same way, which in turn also makes the outer LazyMapSequence a Collection. So calling .first on it will just work without the need for the compiler to pick the eager version of compactMap.

2 Likes

I encountered the same need of wanting to have the first element of a given type several times, especially when dealing with UIKit or other framework written in Obj-C.

Regarding the naming, I totally agree with @Jens. If it is tempting to use as as a way to implicitly reference the cast operator, the following expressions are too identical while carrying different meaning:

sequence.first as? Type        // cast first element (if any) to type
sequence.firstAs(Type.self)    // first element of given type, if any

Maybe sequence.first(ofType: Type.self) would be clearer? And if we add a first, we should add a last as well, sequence.last(ofType: Type.self).

Also, I'm not sure how likely the core team would accept this kind of proposal. It sounds like many people have their own flavour of this tiny helper, which could be a good argument to put in the standard library, but on the other hand, it's one liner, so...

3 Likes

I personally prefer those over others that have been suggested in this thread. Although not as universally applicable as other suggestions, these are super expressive and I have used the exact naming in projects of mine before.

Thanks everyone for your thoughts on this! It's seeming as though the idea itself is generally well received with a couple of things that may need further work/thought.

  1. Utility
    a. It seems many would prefer some kind of transform closure to make this function more versatile as opposed to constraining the function to simply finding the first of a type.

  2. Naming
    a. firstAs() — as originally proposed seems to cause some confusion due to its similarity to sequence.first as? MyType
    b. first(ofType:) — an improved version of the original proposal
    c. firstMap(_ transform:) — some think it may be too close to sequence.first.map
    d. firstResult(_ transform:)
    e. firstNonNilValue(_ transform:)
    f. first(_ transform:) — I believe this will cause ambiguity w/ first(where:) when using trailing closures

  3. Default argument
    a. People seemed to think using type inference by omitting an explicit argument would be confusing as it doesn't read well. Note: This problem would be resolved if this were to more to a more general transform closure style

I personally find myself leaning toward first(ofType:) or firstMap(_ transform:) at this juncture depending on the use of a closure vs a type argument.

So, when T is Element, this method always succeeds, and when they differ the method always fails, right? It seems that, instead of this method existing at all, some surrounding code should have a generic "where T == Element" attached to it. (Maybe there's some utility if T subclasses Element?)

(Maybe there's some utility if T subclasses Element ?)

That's the usecase given in the original pitch:

let label: UILabel?

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

Are you guys sure that you need to handle non-collection sequences? If your subviews are in a Collection, then just use the existing first computed property:

class MyBase {}
class MyDerived: MyBase {}

let classArray: [MyBase] = [MyBase(), MyBase(), MyDerived()]
if (classArray.first as? MyDerived) != nil {
    print("Hi")
}
if (classArray.reversed().first as? MyDerived) != nil {
    print("Hello")
}

// Only "Hello" is printed.

Sequence doesn't have anything like first because a general Swift sequence may be single-pass, so grabbing the first element could ruin access to later elements.

The firstAs method as described in the original pitch would print both "Hi" and "Hello", since it returns the first element of the given type, not just the first element of the collection. I agree the naming is confusing (and I can't say I'm totally sold on the feature anyway).

Oh, that would be:

classArray.compactMap { $0 as? MyDerived }.first

(Checking it for nil, of course.) That's one of the options in the original post. Does the OP need to access non-collection sequences? If not, just use the code above. Otherwise, they should petition for a version of first for Sequence. (Not necessarily with that name. Or use .first(where: { _ in true }), as seen earlier in the thread.)