The missing `Sequence.first`

Quite frequently I find myself stumbling over the fact that there is no

extension Sequence {
    var first: Element? {
        var iterator = self.makeIterator()
        return iterator.next()
    }
}

which I’d consider a small and obvious addition to the language. Is there a particular reason for this omission?

Relatedly, [Concurrency] AsyncSequence seeks to introduce the analogous

extension AsyncSequence {
  public func first() async rethrows -> AsyncIterator.Element?
}
3 Likes

I think that before this is/isn't added, we should clarify whether Sequence is expected to be repeatedly iterable or not.

A Sequence.first like this would consume the first entry, which might be forever gone, depending on the particular sequence's implementation.

2 Likes

Sequence does not guarantee that conforming types are multi-pass; first, if added, would be allowed to consume the first element.

What types are you using that conform to Sequence but not Collection?

(It should also be noted that there is Sequence.first(where:). You can write first { _ in true }. Part of the thinking reflected in this design is that it is more surprising for a property to potentially consume elements of a sequence when invoked.)

5 Likes

Which is what I end up writing usually.

Well, I’ve personally never had an issue with that. But I find it quite surprising that something as innocuous as

.compactMap { $0.cycled().dropFirst(n).first }

puts my laptop in leaf blower mode. The fact that code in this functional style sometimes works (consuming a Collection; without .cycled()) and sometimes doesn’t (consuming a Sequence).

2 Likes

Not just the first element; the whole Sequence.

Once you call .first, any other attempt to inspect the sequence's elements is allowed to produce arbitrary results.

My personal pet peeve is that the language won't accept:

let first = seq.makeIterator().next()

And generally won't compile mutating methods on ephemeral temporary values:

let target = makeTemp().mutatingMethod()

I don't know what's the reason why it is disallowed to create and immediately discard mutating values. It just forces to declare a temp var without clear benefit.

If this were allowed, this thread would not exist in the first place, answers about the fundamental nature of sequences would be spared to the OP. This is an ergonomic problem with the language, not a problem with Sequence, or the OP's goals.

3 Likes

Consider code like

let w = foo.words.remove(at: 0)

You can’t tell from this call site, but words is read-only! If mutating methods were allowed on temporaries, this call would be accepted by the compiler, even though it’s clearly wrong.

2 Likes

In all scenario this problem happens, people just do introduce the missing var, and there lies the ergonomic problem:

var words = foo.words
let w = words.remove(at: 0)

I'm sure the compiler could help. I think it should.

Well, that's only because we have the extremely concise, and completely mutationally consistent

AnyIterator(seq.makeIterator()).next()

and

AnySequence(seq).makeIterator().next()

available to us! :smiley_cat:

:thinking:

(I use a Sequence.first extension all the time.)

More confusing behavior caused by the absence of Sequence.first:

Obviously slow:

(0...10_000_000).map { $0 }.first

Fast, as one would expect:

(0...10_000_000).lazy.map { $0 }.first
(0...10_000_000).lazy.map { $0 }.first { _ in true }

Expected infinite loop:

(1...).map { $0 }

Unexpectedly:

(1...).lazy.map { $0 }.first  // (Function)
(1...).lazy.map { $0 }.first! // Infinite loop!

(Which, of course, can be fixed by defining your own Sequence.first.)

Not an infinite loop, as one would expect:

(1...).lazy.map { $0 }.first { _ in true }
1 Like

This part could also be "fixed" by requiring .first(where:) instead of .first

1 Like

The first line yes, but not the infinite loop.