[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.

11 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.

2 Likes

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.

2 Likes

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

I'm starting to recognize that people want the any variant of this, as well as the some one.

1 Like

What if you could write:

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

and the constraints on the generic parameters would be inferred from Range's requirements? That avoids the need to restate the bounds, while also keeping the number and position of implied generic parameters easy to read for someone looking at the declaration. We infer bounds on T when you write func foo<T>(_: Range<T>), so this seems like a nice consistent shorthand.

3 Likes

This already works via the usual requirement inference mechanism:

func substring(_ range: Range<some Any>)

So if you parsed some as some Any it might just work.

2 Likes

The interesting thing I would like is the any variant:

struct Generic<T> {
  let value: T
}

func something() -> any Generic {
  if Bool.random() {
    return Generic(value: 123)
  } else {
    return Generic(value: "hello world")
}

An existential type any P & Q is really something like this, we’re that syntax to exist: any <T where T: P, T: Q> T; that is, it’s a container for a value of type T which is a generic parameter in a local generic signature with some requirements imposed on it. Similarly any Sequence<Int> becomes any <T where T: Sequence, T.Element == Int> T.

Your any Array would become any <T> Array<T> in the more general syntax; so it’s a container storing an Array<T> (not just T) and the T is type erased. You could also have more than one erased parameter, for eg any Dictionary.

Not sure we’ll ever get “fully generalized existentials” in the surface language, but this is fact how they’re modeled internally in the compiler.

(If I ever get around to finishing it, Part II of Compiling Swift generics has a chapter on existential types).

9 Likes

please can we surface this? this would be immensely helpful.

2 Likes

We need this kind of control sometimes…

func ƒ(_: [String: some Any]) { }
func ƒ(_: [some Any: String]) { }

…but otherwise, I don't think it's so good when there's more than one placeholder type.

func ƒ(_: [some Any: some Any]) { } // compiles
func ƒ(_: some Dictionary) { } // doesn't; would be better

Being able to put some in either place for types with a single generic placeholder, as per your whim, seems consistent with case-binding:

switch "🇸🇴" as Optional {
case .some(let _): break
case let .some: break
default: break
}

I think that's where I disagree it's better. Although Dictionary is a common type that people are familiar with, seeing some Foo for an unknown type or protocol would become a lot more ambiguous, since it could be either introducing one generic parameter bound to a protocol named Foo, or any number of generic parameters corresponding to the parameters of a generic type Foo.

A reasonable midpoint might be to allow for variadic implicit generic parameters, so you could write

func f(_: Dictionary<some...>)

or whatever syntax we decide for variadic type variables as a way to keep it clear that multiple type parameters are being introduced.

4 Likes

This probably deserves its own thread, but occasionally I find myself reaching for TypeScript's infer operator. Like let, infer creates a new binding, but that binding is to a type. Here's an example I just encountered:

    // Inside a generic struct
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Key.self)
        if Value.self == Optional<infer Wrapped>.self {
            value = try container.decodeIfPresent(Wrapped.self, forKey: key)
        } else {
            value = try container.decode(Value.self, forKey: key)
        }
    }

The design of Codable aside (as there might be a better way to do this), I do occasionally wish something like this were possible.