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
- 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. <
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.
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 hideous “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();
3. It makes the uses of brackets confusing and inconsistent
Many legacy languages use <
and >
for comparisons, bit-shifts and generics, as well as both ()
and []
for function calls.
Instead, imagine a design where each bracket has a clearly-defined use …
[]
encloses type parameters or type arguments
()
groups expressions, parameter/argument lists or tuples
{}
sequences statements or definitions
… and <
/>
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 syntax6:
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:7
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 number of languages using []
for generics seems to increase lately –
with Scala, Python, and Nim joining Eiffel, which was pretty much the sole user of []
for decades.
It remains to be seen whether this turns into tidal change similar to the widespread
adoption of
With the recent adoption of ident: Type
over Type ident
in modern languages.[]
for generics by Go and Carbon this seems to be the likely outcome.
-
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 ↩
-
Nim uses
[]
for generics, but employs a hack to also use[]
for lookup. ↩ -
Python and Scala demonstrate that this approach works incredibly well. ↩