SE-0516: Borrowing Sequence

Hello, Swift community!

The review of SE-0516: Borrowing Sequence begins now and runs through March 17, 2026.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager by email or DM. When contacting the review manager directly, please put "SE-0516" in the subject line.

Trying it out

The toolchain with the latest implementation is currently building - I will update the review thread with the toolchain links for macOS, Linux, and Windows when they're ready later today.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at:

swift-evolution/process.md at main · swiftlang/swift-evolution · GitHub

Thank you,

Holly Borla
Review Manager

9 Likes

I feel like this is a miss for a few reasons:

  • It's a really clunky protocol; it'd be a shame for this to be "the way to write sequences". Sequence is pretty easy. Rust's Iterator is easier in most circumstances. This is worse.
  • It seems to be "too soon" — it doesn't handle nonescaping elements, and doesn't seem like it'll be extensible to nonescaping elements in the future, given that they require lifetime annotations. I don't think nonescaping elements are an edge-case or uncommon need. So won't we just be back here in 6-12 months when lifetimes have caught up, trying to specify BorrowingNonescapingSequence?
  • It seems to be "too soon" — implementing it requires using the Lifetimes experimental feature. This was promised to be stable, but broke source-compatibility between 6.2 and 6.3 already. I don't trust that implementations of BorrowingSequence made for 6.4 will compile in 6.5.
  • I don't like that this isn't "replacing" Sequence, since there are cases that Sequence supports but BorrowingSequence does not. I'd like to see more discussion of whether there is a design that allows for a true one-size-fits-all?

Thoughts about a more one-size-fits-all solution:

Are we just ignoring Rust's prior art here? In Rust:

  • Iterator can be over values or borrows, and might or might not itself be escaping depending on which one.
  • There's only standardization on how to get a consuming iterator from a container. This means that for loops can be less ergonomic (for item in container consumes the container, for item in container.iter() is an ad-hoc standard to get a borrowing iterator, but it allows for other options like for item in container.drain(3..14) to get an iterator that consumes items in a specified range of a Vec, for example.

So what if,

  • We adjust IteratorProtocol to allow nonescaping implementors & nonescaping Element
  • The compiler's for syntax gets special support for IteratorProtocol<T> (current), IteratorProtocol<Borrow<T>> (new) and IteratorProtocol<Span<T>> (new):
    • When iterating sequences of Borrow, the iteration variable is implicitly shorthand for borrow.value (so you don't have to manually deref the borrow on each use within the loop)
    • When iterating sequences of Span, the iteration variable is implicitly shorthand for span[i]. This is more or less equivalent to BorrowingIteratorProtocol from the current proposal.
  • Sequence remains shorthand for "i can get a copying, escapable iterator"
  • We create an ad-hoc standard for getting a borrowing iterator, eg. for element in container.borrow(). I suggest this because it avoids ambiguity over "which kind of iterator is this for loop using" compared to having both Sequence and BorrowingSequence, and also makes it clear that offering other kinds of iterator through other ad-hoc methods is reasonable (see drain)
19 Likes

Would this support for inout element in array { ... } ?

1 Like

Another alternative direction could be to introduce the ability to syntactically overload the for loop without conforming to a protocol.

With property wrappers, SE-0258 explains the rationale for using a syntactic approach instead of a protocol:

I think a similar argument applies to iteration. Just like the wrappedValue property of a property wrapper (or the subscript operator of an arbitrary type), iteration can have many different kinds of ownership/mutability for both the collection and its elements. Furthermore, different iterators can have different lifetime semantics. The lifetime of elements yielded from a Span is different from those yielded from an Array, which is different from those yielded from a Range. Non-escapable elements would introduce even more nuances. The proposal acknowledges that the lifetime annotation @_lifetime(&self) is, for some types, more restrictive than necessary. I also think it's probably too early to decide which lifetime semantics would be most useful for generic algorithms. (Edit: The throws and async effects also add other dimensions of variation.)

Personally, I think it would be nice if writing a coroutine/generator became the syntax for overloading the for loop.

5 Likes

Comparing this to the for loop section of the Ownership Manifesto, it seems like the intention there was to use ownership declarations on the iteration variable to distinguish between different types of iteration, e.g.

for borrowing employee in company.employees {
  newCompany.employees.append(employee)
}

Would this be a way for users of the API to be able to choose whether they want iteration from Sequence or BorrowingSequence?

The manifesto also shows generators as the mechanism by which other types of iteration could be implemented. Was that considered as an option for this?

8 Likes
  /// Advances this iterator by up to the specified number of elements and
  /// returns the number of elements that were actually skipped.
  mutating func skip(by maximumOffset: Int) -> Int

Can this intentionally under-skip? ie, could this return 0 even if it’s not at the end?

1 Like

Just chiming in here with a few nitty-gritty API naming comments:

  /// Returns a span over the next group of contiguous elements, up to the
  /// specified maximum number.
  @lifetime(&self)
  mutating func nextSpan(maximumCount: Int) -> Span<Element>

Standard library convention is to use "max" rather than "maximum" (with the limited exception of a specific IEEE floating-point API, which is its own bag of fish).

Additionally, there's no need here to state the return type in the name of this API—there isn't a need to disambiguate from next() -> Element as they differ in the number of arguments, and Span is an entirely reasonable return type (and probably the only one): therefore, something like next(maxCount: 4) would suffice.

(Nit: Elsewhere, "up to n" is the terminology we use for 0..<n, whereas I believe the intention here is that the count of the returned span is the closed range 0...n.)

Finally, it would be helpful to specify—and I believe all the stated use cases could guarantee this—that the returned span is empty if and only if we're at the end. The text strongly suggests that is the case, but in parts it technically leaves open the possibility of a conforming type which sporadically returns an empty span and then subsequently a non-empty span.


  /// Advances this iterator by up to the specified number of elements and
  /// returns the number of elements that were actually skipped.
  mutating func skip(by maximumOffset: Int) -> Int

Standard library precedent already includes the term "offset"—e.g., index(_:offsetBy:limitedBy:)—or "advance"—e.g. advanced(by:). I think it's fair to avoid using drop or pop (given differences in how those APIs behave), but we don't need to coin yet another verb here.

Given that "offset" was already thought fit for the internal parameter name, I think it's a great choice here for the API itself, clearer than "skip" as it eliminates doubts as to inclusivity of the start or end. If the ambiguity as to whether that's a verb or noun is thought to be disqualifying, then advance(by:) is fine too for an active verb, in my view.

It would be good to clarify the scenarios under which the supplied argument isn't the number of elements actually offset or advanced: if it's only when there aren't enough elements at the end, then by: is great as an argument label--and I wonder if the API should be @discardableResult; if there are other scenarios in which it may happen other than running off the end, then byMax: would be more fitting here to emphasize that (though I wonder if it should still be @discardableResult).

5 Likes

I’d like to note a subtle lifetime quirk. The proposed BorrowingIteratorProtocol’s nextSpan looks like this:

public protocol BorrowingIteratorProtocol<Element>: ~Copyable, ~Escapable {
  associatedtype Element: ~Copyable
  
  /// Returns a span over the next group of contiguous elements, up to the
  /// specified maximum number.
  @lifetime(&self)
  mutating func nextSpan(maximumCount: Int) -> Span<Element>
  
  // ...
}

The lifetime of the returned Span is tied to the lifetime of the iterator, not the sequence. This means it would not be possible to write generic algorithms that need to persist a borrow of an element beyond the scope of a for-loop. For example, take this generic search over noncopyable elements:

extension BorrowingSequence 
    where Self: ~Copyable & ~Escapable, 
          Element: ~Copyable, Equatable {

  @lifetime(borrow self)
  func first(_ e: Element) -> Borrow<Element>? {
    var found: Borrow<Element>? = nil
    for x in self {
      if x == e {
        found = x
        break
      }
    }
    return found
  }
}

I’m trying to find and return a Borrow of the first matching element, but I can’t write it like this. Based on this proposal, the function body would correspond to this desugaring:

@lifetime(borrow self)
func first(_ e: Element) -> Borrow<Element>? {
  var found: Borrow<Element>? = nil
  do {  // desugared for-in loop with early 'break'
    var iterator = self.makeBorrowingIterator()
    var endIteration = false
    while !endIteration {
      let span = iterator.nextSpan(maximumCount: Int.max)
      if span.isEmpty { break }
      for i in span.indices {
        let x: Borrow<Element> = span[i]
        if x == e {
          found = x   // <-- illegal escape of the borrow 'x' to outer scope
          endIteration = true
          break
        }
      }
    }
  } // end 'do'
  return found
}

The do-block denotes the lexical scope of the original for-in loop. The lifetime of each Span returned by nextSpan is tied to the var iterator ‘s lifetime, which ends whenever the loop exits. So, I can’t store a borrow of the found element outside this do-block (i.e., the original for-loop) regardless of whether I use Borrow<T> or a pair of Span<Element> + an index.

I can only workaround this by taking the iterator as an argument to my first function and writing-out the desugared version using it, which I think is rather clumsy.

For algorithms that need to make two-passes over a BorrowingSequence within the same function, where the first pass is collecting borrows of interesting elements, the for-in syntax won’t work either. I’d have to drop down to using makeBorrowingIterator manually to keep the first iterator live during the second pass.

5 Likes