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.

), 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 as binary comparison operators (and in binary bitshift operators) 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:

Java approached these issues by making the syntax less consistent – which is the reason why Java’s definition-site syntax for Generics and its use-site syntax differs substantially:

// class definition/instantiation: type parameter after name class Foo < T > {} new Foo < String >(); // method definition/invocation: type parameter before name < T > void foo () { ... } instance .< String > foo ();

C# and Kotlin tried to retain a more consistent syntax by introducing unlimited look-ahead: Their parser just keeps reading input after the < until it can make a decision.

For decades, C++ required adding 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.

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 allows [] to be (ab)used for syntax “conveniences”

Many languages used [] to add syntax for collection literals ( [1, 2, 3] ) or array lookup ( array[0] ), adding pointless complexity to the language for very little benefit – as such built-in syntax usually becomes dead weight a few years down the road, as the preferred choice of data structure implementation evolves.

Using [] for generics instead of <> shuts down this possibility for good, and encourages the use of standard method call syntax for these usecases instead:

Array(1, 2, 3) /* instead of */ [1, 2, 3] 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.