[Pitch] Use “some” to express “some specialization of a generic type”

Proposal

The some keyword neatly replaces simple cases of <T> where T: Protocol. That said, I think there is an opportunity for a bit of syntactic sugar to elevate the readability of generics. In particular, I think it would be nice if Generic<some A, some B> could also be written as some Generic where A and B are the protocol requirements of Generic's type parameters.

Reason

The most obvious benefit of this is preventing generic type nesting beyond the point where every possible specialization is valid. Imagine that you are writing a String method that takes a range of positions and returns a Substring, where a position is generic over some encoding like UTF8, UTF16, or Character. Two equivalent function signatures, with and without syntactic sugar, may look like this:

func substring(_ range: Range<some Position>) -> Substring
func substring(_ range: Range<Position<some Encoding>>) -> Substring

Having written methods similar to this, it is apparent that the nesting is redundant because every specialization of Position is a Position<some Encoding>. I think, therefore, that allowing the former, a shorter and unambiguously equivalent signature, as shorthand for the latter would improve the readability of generics in cases such as the one described.

8 Likes

I noticed that the compiler infers T: Encoding from Position for <T> syntax:

func substring<T>(_ range: Range<Position<T>>) -> Substring // OK
func substring<T>(_ range: Range<Position<T>>) -> Substring where T: Encoding // OK

I think this illustrates an expectation that similar inference should be available with some.

1 Like

I am definitely intrigued by this idea! I haven’t thought through all the implications, but broadly it makes sense to me not to have to manually, syntactically ‘forward’ all the generic arguments to parameters you’re accepting.

A couple questions that immediately come to mind are:

  • Would we support some S in return position? Currently there’s no equivalent ‘long hand’ syntax, but we also don’t have that for some P in return position so maybe that’s fine.
  • Would we support unbound generic types as generic constraints themselves? Currently some P in argument position is always equivalent to <T> where T: P, and it might be nice to keep the symmetry (i.e., by letting users write <S> where S: Set instead of generalizing over the element type specifically.
2 Likes

I think supporting opaque result types is fine, and I found that this idea is actually extending the range of types which can be expressed in Swift;

struct Foo<Wrapped: Collection<Int>> where Wrapped.Index == Int {
    var wrapped: Wrapped
}
var foo: Foo<some Collection<Int where Index == Int>> // there is no corresponding expression for `some`
var foo: some Foo // with the idea

This is definitely needed. The asymmetry we have at the moment doesn't make any sense.

// Compiles.
func ƒ(_: some BidirectionalCollection & RandomAccessCollection & ExpressibleByArrayLiteral) { }

// Reference to generic type 'Array' requires arguments in <...>
func ƒ(_: some Array) { } 

That said, I can't tell if your examples should actually compile—is some Position really meaningful enough?. It would be nice to have examples that either don't rely on code we don't have, or if you could provide the code.

1 Like

I see prefixing a generic type with some as a readable way of propagating inference of type parameter conformances. I realize this depends on a some _ placeholder capable of doing this for a single type parameter, which perhaps should have been the core of this pitch. Given a type struct Foo<A, B> where A: P, B: Q, some Foo would mean some Foo<_, _> which would mean Foo<some _, some _> which would mean Foo<some P, some Q>.

To be clear, some Foo<Int> would be rejected, right?

Yes, I think you would have to specify both type parameters. Maybe when generic opaque type constraints are available, it would make sense to use the same syntax in this case as well and write something like: some Foo<.B == Int>, which would be the same as some Foo<_, Int> or Foo<some _, Int> with the pitched syntax and Foo<some P, Int> with the current syntax. I think some Foo<Int> only makes sense if there exists an analog to primary associated types.

This has occurred to me as well, but I suppose it only makes sense for input parameters, e.g. some Array could not be a return type. The function implementer would know what Element is but as a caller you wouldn't. Unless the idea is that the actual type is revealed to you implicitly:

func foo() -> some Array { 
   [1, 2, 3]
}

let x = foo() // x is now [Int]

EDIT:

Turns out this already works for some Collection, so I guess it would make sense for some Array as well.

  func foo() -> some Collection {
    [1, 2, 3]
  }

  func bar() {
    let baz = foo() // some Collection in XCode
    print(type(of: baz)) // prints Array<Int>
  }

For now. I haven't seen people clamoring for it but that doesn't mean it's not useful for anything.

E.g. I never found a use for some Sequence, even though I was just able to get rid of a bunch of usage of AnySequence in favor of some Sequence<ConcreteType>. And some Sequence still compiles. So why shouldn't every generic type as well?

I like the idea a lot! Would love to hear any doubts or counter arguments, but my initial reaction is +1