Language Design: Stop Using <> for Generics

Published on 2020-04-04. Last updated on 2022-07-30

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() { ... }
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.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 ident: Type over Type ident in modern languages. With the recent adoption of [] for generics by Go and Carbon this seems to be the likely outcome.

  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. Nim uses [] for generics, but employs a hack to also use [] for lookup

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