I really like this part of your pitch. I agree with others that introducing generics into structs and other types this way feels pretty weird, but I like it as a convenient way to make functions and subscripts generic.
Some people may not prefer them, but I actually like that it keeps the angle brackets. It maintains parity with other generic code: the traditional generic function syntax, generic types like Result, etc. I think your syntax improves greatly on the legibility without introducing new visual cues (like any or some) which is great.
It has been suggested in another thread that some should maybe be reserved only for opaque return types and not used as a convenient way to introduce anonymous generics.
I am inclined to agree. Opaque return types are confusing enough at first. Overloading the same keyword to also create “normal” generics just muddies the water more, I think.
A quick justification
My rationale for not overloading the meaning of some goes like this:
Suppose I’m new to learning generics. I learn about the traditional syntax first, for whatever reason. So I write some functions like this:
func send<Input: Numeric>(input: Input)
func receive<Output: Numeric>() -> Output
I later learn that “some can be used to introduce anonymous generics,” i.e. it’s just some shorthand syntax. I like that so I refactor my functions as such:
func send(input: some Numeric)
func receive() -> some Numeric
But now, I’ve just changed the meaning of my receive function, because some has a different meaning when used with a return type. It makes the type “opaque” not “generic” as my original function was written.
The explanation of some would need to be much more nuanced because it’s interpretation depends on how you’re using it.
Furthermore it would be pretty unfortunate that in order to have a generic return type I would have to keep the traditional generic syntax and can’t use the more convenient some keyword.
That’s why I like your design. I can just cut and paste the generic signature on the end of the function signature.
So, if it is too problematic to overload some to mean “anonymous generic” then I think your proposed syntax here is a good alternative.
I think the concern about a “sneaky” generic appearing at the end of a long declaration like this is a very warranted. Especially upon seeing the example Syre provided!
To alleviate this, we could require that a generic type must appear lexically before other points of use. So according to that rule, this example you gave would have to change:
// error: ‘P.Meal’ cannot appear before generic ‘P’ is introduced.
func feed(food: P.Meal, to person: <P: Person>) -> P.Thanks
// A Fix-it could move <P> to the traditional position for generics.
func feed<P: Person>(food: P.Meal, to person: P) -> P.Thanks
I think this rule is pretty easy to justify and explain.
Finding new syntax for protocol existentials might be important to consider here. There have been lots of different proposals, like any Collection and any Collection<.Element == Int> and so on.
The core of your pitch only addresses the ergonomics and legibility of generic function declarations, but doesn’t really help the existential syntax situation. Maybe that’s fine though — existentials are a related but still pretty different language feature.
I don’t think what you’re proposing would prohibit future efforts to improve the syntax and expressiveness of protocol existentials.
Cheers! Thanks for reading