It might be worth sketching out another idea I had for how we could make generic functions a bit easier to read and write, since it also involves repurposing angle brackets (albeit in a different way).
I mentioned in the other thread how I'd like us to unify existentials and generics in contexts where they are the same thing, and one of those contexts is using an existential function parameter as an anonymous generic type:
// Today:
func frobnicate<C>(strings: C) where C: Collection, C.Element: StringProtocol
// If unified with existentials, this would be the same as:
func frobnicate(strings: Collection where Element: StringProtocol)
But the other thing I think is quite interesting is reintroducing angle brackets within the parameters for named generic types:
// How do we simplify this?
func frobnicate<C>(
strings: C,
from index: C.Index
) where C: Collection, C.Element: StringProtocol
// Bring the generic signature in-line:
func frobnicate(
strings: <C: Collection where Element: StringProtocol>,
from index: C.Index
)
Currently, when you read a generic function, you see (in order)
func {function-name}<X, Y, Z>(
names: X, positions: Y, colors: Z
) -> Int where
X: Collection, X.Element == String,
Y: Collection, Y.Element == Coordinate,
Z: Collection, Z.Element == Color
-
func
. Simple enough. -
{function-name}
. A short description of the function, chosen to make sense at the call-site. -
A prelude
<X, Y, Z>
, where the function defines some placeholder types called X, Y, and Z which it will use later in some way. These are usually poor quality names, because they don't have any meaning besides what they represent in the signature to follow. -
The function arguments - with descriptive names (
strings
,names
,colors
), but illegible type information (C
,X
,Z
, etc). We can likely guess from the argument names what these generic types will involve, but that information is still not written out for us yet. -
The function's return type (if any)
-
The list of constraints which finally tell us what X, Y, and Z actually are. Flick back and forth between the function signature and the constraints a few times while you match them up. Also, note that we need to keep repeating the names X, Y, and Z, because they share a single constraints list (
X: Collection, X.Element == String
, etc).
When you step back and look at it, we chop up the information about generic types and scatter it about the function signature like we're almost trying to hide it. Deciphering a generic function is a lot more convoluted than your typical, non-generic function, and information locality is a big reason for it, IMO.
When I see a function argument with a name like strings:
or names:
, that is usually immediately followed by a colon, then the argument's type. If the examples above were not generic functions, I would seeing nice, legible signatures such as:
func frobnicate(strings: [String])
func plotMarkers(names: [String], positions: [Coordinate], colors: [Color])
I can read those in a single left-to-right pass with no backtracking.
If we brought this same approach to generics, I think it could make even complex signatures much simpler:
// Non-generic, for comparison.
func plotMarkers(
names: [String],
positions: [Coordinate],
colors: [Color]
) -> Int
// Today:
func plotMarkers<Names, Positions, Colors>(
names: Names, positions: Positions, colors: Colors
) -> Int where
Names: Collection, Names.Element == String,
Positions: Collection, Positions.Element == Coordinate,
Colors: Collection, Colors.Element == Color
// Possible future:
// Level 1 - anonymous generic parameters.
func plotMarkers(
names: Collection where Element == String,
positions: Collection where Element == Coordinate,
colors: Collection where Element == Color
) -> Int
// Possible future:
// Level 2 - named generic parameters.
func plotMarkers(
names: <Names: Collection where Element == String>,
positions: <Positions: Collection where Element == Coordinate>,
colors: <Colors: Collection where Element == Color>
) -> Int
Of course, I'm biased, but I think this is perhaps a more promising future direction for angle brackets. The reason I bring it up with this pitch is that, if we used angle brackets for same-type constraints as well, I think this idea would be significantly less appealing.