Language Design: Stop Using <> for Generics

Published on 2020-04-04.

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 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:1

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:2

// class definition/instantiation: type parameter after name
class Foo<T> {}
new Foo<String>();
// method definition/invocation: type parameter before name
<T> void 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.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 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 – in many cases such built-in syntax became dead weight after the languages’ preferred choice of data structure implementation evolved.67

Using [] for generics instead of <> shuts down this possibility for good, and encourages the use of standard method call brackets (()) for these use-cases instead:8

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

At this stage, some small amount of syntax sugar can be considered that would allow every type with a get method to be written as instance(arg) and a set method written as instance(index, arg), leading to the following code:9

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"


Thankfully, the number of languages using [] for generics seems to increase lately – with Scala, Python, Nim and Go 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 ident: Type over Type ident in modern languages.

  1. Parsing Ambiguity: Type Argument v. Less Than is a similar article focusing on some of these issues in more depth. 

  2. 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. 

  3. C#: See ECMA-334, 4th Edition, §9.2.3 – Grammar Ambiguities 

  4. C++: See What are all the syntax problems introduced by the usage of angle brackets in C++ templates? 

  5. C++: See Wikipedia – C++11 right angle bracket 

  6. Java pretty much abandoned arrays – they never integrated them with collections in 1.2, let alone generics in 1.5. 

  7. JavaScript stopped giving out new collection literals almost immediately after its first release – no collection type added since received its own literals (Set, Map, ByteBuffer, …). 

  8. Nim uses [] for generics, but employs a hack to also use [] for lookup

  9. Python and Scala demonstrate that this approach works incredibly well.