Language Design: Stop Using <> for Generics
TL;DR: Use [] instead of <> for generics. It will save you a lot of avoidable trouble down the road.
1. <> is hard to read for humans
< and > are usually already used as comparison and bitshift operators, which (as binary operators)
conform to a completely different grammatical structure compared to their use as brackets.
This, often in combination with other design mistakes – like the use of both () and [] for function calls,
or literal syntax to construct lists, sets and maps – leads to a confusing and inconsistent use of brackets in many languages.
On top of that, imagine a programming font that in which the height of parentheses (()), curly braces ({}) or
square brackets ([]) were capped to the height of a lower-case letter.
This is of course ridiculous – but exactly what happens with the (ab)use of the <> symbols for brackets.
2. <> is hard to parse for compilers
Many languages that were created without generics in mind had trouble adding generics later on,
as all pairs of brackets – ( and ), { and }, [ and ] – were already put to use.
< and >, used in binary operators for comparisons and bitshifts, were usually the only symbols left in the
grammar that are practical to overload with a new, different meaning.
That’s pretty much the only reason why <> started to be used as generics in the first place.
Unfortunately, using < and > for generics caused parsing problems in every language that tried
use them for this purpose, forcing language designers to indulge in various ugly workarounds:1
Java approached these issues by making the syntax less consistent (before function return types, but after class names) and limiting where generics can be used (for instance not with statically imported methods).2
C# and Kotlin tried to retain a more consistent syntax by introducing unlimited look-ahead:
Their parsers keep reading input after the < until a decision can be made.3
C++ suffers from a plethora of <>-related issues.4
The only issue addressed by the C++ committee after decades was the requirement to add spaces to
nested closing generics to allow the compiler to distinguish between the right-shift operator >>
and the end of a nested generic type definition.5 All other issues appear to be unfixable.
Rust is forced to use the so-called “turbofish” operator ::<> to distinguish between the left
side of a comparison and the start of a generic type, introducing syntactic inconsistency between
generics in a type context and generics in a term context:
let vec: Vec<u32> = Vec::<u32>::new();
/*or*/ <Vec::<u32>>::new();
/*or*/ <Vec<u32>>::new();
Despite that, even simple code snippets still get caught in the cross-fire:
1 as u32 << 1; // error: `<<` is interpreted as a start of generic arguments for `u32`, not a shift
WGSL’s specification of template lists requires multiple pages of “template list discovery algorithm”6,
hoping to dig itself out of the ambiguities caused by using < and > for comparisons, shifts and
compound assignments in addition to their use for template lists.
A better design
Instead of repeating the mistakes of the past, imagine each bracket has a clearly-defined use:
[]encloses type parameters or type arguments
()groups expressions, parameter/argument lists or tuples
{}sequences statements or definitions
</> is only used as a comparison operator, and not misused as a makeshift bracket.
This substantially simplifies the mental model beginners need to adopt before writing their first program
(”() is for values, [] is for types”), and encourages the elimination of syntactic special cases like collection literals …
Array(1, 2, 3) /* instead of */ [ 1, 2, 3 ]
Set("a", "b", "c") /* instead of */ { "a", "b", "c" }
… and array indexing in favor of standard function call syntax7:
someList.get(0) /* instead of */ someList[0]
array.set(0, 23.42) /* instead of */ array[0] = 23.42
map.set("name", "Joe") /* instead of */ map["name"] = "Joe"
A small amount of syntax sugar can be considered, leading to the following code:8
someList(0) /* instead of */ someList[0]
array(0) = 23.42 /* instead of */ array[0] = 23.42
map("name") = "Joe" /* instead of */ map["name"] = "Joe"
Coda
Thankfully the tide is turning, and an increasing number of languages adopted [] for generics –
with Go, Scala, Python, and Nim joining Eiffel, which was pretty much the sole user of [] for decades.
-
Java: The syntax inconsistency is due to the difficulty a compiler would have to tell whether some token stream of
instance.foo<is the left side of a comparison (with<being the “less-than” operator) or the start of a generic type argument within a method call. ↩ -
C#: See ECMA-334, 4th Edition, §9.2.3 – Grammar Ambiguities ↩
-
C++: See What are all the syntax problems introduced by the usage of angle brackets in C++ templates? ↩
-
C++: See Wikipedia – C++11 right angle bracket ↩
-
WebGPU Shading Language: SeeSpecification – 3.9 Template Lists ↩
-
Nim uses
[]for generics, but employs a hack to also use[]for lookup. ↩ -
Python and Scala demonstrate that this approach works incredibly well. ↩