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:
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.
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.
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.
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>.
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?
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.
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).
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.
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.