Gold-plating UnboundedRange

(This may be a more appropriate topic for Evolution, but here we go…)

The text of SE-0172 never described a facility for unbounded ranges, but nevertheless the implementation ended up growing one. At the moment, it’s definitely creaky; it only works with the one subscript on Collection, not on anything that takes <T: RangeExpression>. This is not only annoying when composing the use of stdlib APIs, but also painful for writing your own APIs that use RangeExpression; this is especially true if it’s a revision of a previous API that took fully-bounded RangeS, because there you could always say Range<X>? = nil.

As an individual contributor, I find myself reaching for ... fairly often to mean an unbounded range and being frustrated when it doesn’t work. As a member of a team, the syntax might as well not be there because I can’t adequately explain to others what special cases it does and does not work in without them already being fully versed in the design of the standard library.

The current approach in the standard library is non-ideal and it would be non-ideal to keep it in place for the standard library’s ABI to get locked in.

Long story short, what can we do?

  • Can we lift the limitations the prevent a free-standing ... from working as a calling-the-operator expression? (i.e., func ...() -> UnboundedRange)
  • Can we lift the limitations that prevent a free-standing ... from working as a named lookup? (i.e., public let ...: UnboundedRange)
  • Can we replace the _UnboundedRange hack with some other kind of sigil (not the Unicode ellipsis, please) that is phased out if of the above can happen?
2 Likes

The typealias could be moved into Collection, and its subscript argument could be the PartialRangeFrom operator function.

extension Collection {
  public typealias UnboundedRange = (Index) -> PartialRangeFrom<Index>

  public subscript(_: UnboundedRange) -> SubSequence {
    return self[startIndex...]
  }
}

@available(*, deprecated: 4.2)
public enum UnboundedRange_ {

  @available(*, deprecated: 4.2)
  public static postfix func ... (_: UnboundedRange_) -> ()
}

@available(*, deprecated: 4.2)
public typealias UnboundedRange = (UnboundedRange_) -> ()

This doesn't solve the RangeExpression issue, but at least the UnboundedRange_ enum can be removed by Swift 5.

Alternatively, the existing typealias could be changed to one of the following.

public typealias UnboundedRange = (AnyIndex) -> PartialRangeFrom<AnyIndex>

public typealias UnboundedRange = (AnyIndex) -> PartialRangeThrough<AnyIndex>

public typealias UnboundedRange = (AnyIndex, AnyIndex) -> ClosedRange<AnyIndex>

Either approach you describe sounds like an improvement!

However, I am adamant that having a RangeExpression type that refers to the entire collection is useful outside of just the primary Collection.subscript(…) -> SubSequence, such as replaceSubrange and all the String / NSRange APIs.

I note that it would certainly make the implementation less bizarre to change the syntax to a subscript without any argument:

let slice = array[]

Implemented as:

extension Collection {
	subscript() -> SubSequence {
		get { return self[startIndex...] }
	}
}

That's not exactly the syntax we want, but I also think the complexity for getting the [...] syntax to work is unwarranted.

4 Likes

@michelf, +1

I didn't realize that your solution works currently (and at least as far back as Swift 4.0.3).

I was thinking of a less elegant design, which would only work if the SE-0148 proposal was fully implemented (to support default arguments for subscript parameters).

I really want to keep empty subscript available as a generalized dereference/unwrap operator idiom. It is incredibly common to have wrapper or accessor types with a primary value (c.f. UnsafePointer.pointee), and the result is a proliferation of .value accessess. I'd like all of those to be spelled [].

1 Like

@dabrahams If the primary value is read-only, would a non-dynamic @callable API be an alternative?

@Ben_Cohen I've got a pull request (ready to merge) in case you don't want to keep the UnboundedRange_ enum as ABI.

https://github.com/apple/swift/pull/18649

Yes, but only for the read-only cases.