15
$\begingroup$

In languages like C++ and Rust, generics use angle brackets. For example, Vec<f64> for a vec of floats, HashMap<String, i64> for a map from strings to ints, or Option<Box<char>> for a boxed char that can be None. This isn't universal, however. For example, Haskell uses spaces.

What are some different syntaxes for generics? What are their pros and cons?

$\endgroup$

9 Answers 9

13
$\begingroup$

First-class types

In this case, we introduce a type for types: U, then we have List : U -> U, so naturally we would use the function call syntax for generics: List(Int) or like in Haskell/ML: List Int.

In case of the Map<Thing<String, int>, Option<Box<char>>> example in the other answer, we would have

  • Map : (U, U) -> U
  • Thing : (U, U) -> U
  • Option : U -> U
  • Box : U -> U
  • char : U

So it would be Map(Thing(String, int), Option(Box(char))).

$\endgroup$
5
  • $\begingroup$ This is my favourite syntax, but doesn't type-in-type cause issues? $\endgroup$
    – wizzwizz4
    May 16 at 21:49
  • 3
    $\begingroup$ @wizzwizz4 Not immediately. If U : U then yes, you're up a creek. But if you just stop and say "U is a special snowflake that's not an instance of U or anything else" then you're fine. Alternatively, if you want to go all the way, you can have cumulative universes and say U : U1 : U2 : U3 : U4 : ... ad infinitum. This is how Agda does it. $\endgroup$ May 16 at 22:03
  • $\begingroup$ @wizzwizz4 what kind of issue are you thinking of? Regarding logical consistency, please see my other post: languagedesign.stackexchange.com/a/149/40. If it's other issues, ask me! $\endgroup$
    – ice1000
    May 16 at 22:17
  • $\begingroup$ Note that by itself, this approach will force you to explicitly pass types to all functions, for example map Int [1,2,3] (n -> n+1). Some languages (Coq, Lean) solve this by allowing functions to have inferred arguments (not just types). $\endgroup$
    – xigoi
    Jul 19 at 7:37
  • $\begingroup$ @SilvioMayolo Haskell has kinds which are used to express the "types" of types. The basic kind is called *, so Map :: * -> * -> *, Thing :: * -> * -> *, Option :: * -> *, Box :: * -> *, and Char :: *. But * :: *, so it's not fatal. $\endgroup$
    – A. R.
    Jul 24 at 21:01
13
$\begingroup$

Square Brackets

Python's optional typing module uses square brackets for this, such as list[int]. The PEP that added this syntax has a lengthy analysis of why they chose that syntax, primarily that it didn't clash with any other syntax whereas angle brackets could be confused with less than and greater than signs.

$\endgroup$
4
  • $\begingroup$ Go uses square brackets as well, such as in type Node[T] = struct[T]{...}. The reasoning for square brackets can be seen in the proposal paper here. $\endgroup$
    – bigyihsuan
    Jun 28 at 16:43
  • $\begingroup$ Also used in Nim (which has a Python-inspired syntax, but had generics long before Python). $\endgroup$
    – xigoi
    Jul 19 at 7:33
  • 2
    $\begingroup$ "primarily that it didn't clash with any other syntax whereas angle brackets could be confused with less than and greater than signs." more precisely, I would say: square brackets correspond to an already existing operator that could be overloaded - list[int] could be parsed with existing rules and transform to list.__getitem__(int), whereas list<int> would seem to be missing a right-hand side for the >. Of course, () would also work with existing syntax, but list(int) already has meaning (and is a type error, as the int type isn't a sequence). $\endgroup$ Oct 14 at 23:08
  • $\begingroup$ Yeah, that's a better way of phrasing my point. $\endgroup$
    – pppery
    Oct 15 at 2:46
8
$\begingroup$

Just, parentheses

This is what D does. For example, the following class in C#:

public class Bar<T>
{
    public T member;
}

looks like this in D:

class Bar(T)
{
    T member;
}

Source

Templates are instantiated with an exclamation point, to avoid confusion with regular function calls:

T square(T)(T t)
{
    return t * t;
}
writefln("The square of %s is %s", 3, square!(int)(3));

This has the obvious con of still being confused with a function call, and it also precludes postfix ! from having another meaning (e.g. non-null assertion, as in Swift, C#, and TypeScript).

One thing it has going for it is that it's impossible to confuse with comparisons (see also Rust's turbofish).

$\endgroup$
7
$\begingroup$

Spaces

This would look like List float, Map String int, or Option Box char.

Pros:

  • Looks nicer (in my opinion), reduces clutter
  • Faster to type
  • Angle brackets can be inconvenient due to ambiguity or text editors failing to highlight matched pairs

Cons:

  • Prefix notation can be difficult to read with nested multiple-argument generics (for example, Map<Thing<String, int>, Option<Box<char>>> would be Map Thing String int Option Box char, which takes some work to mentally parse)
  • Incompatible with variadic generics
$\endgroup$
9
  • $\begingroup$ Another con: incompatible with variadic generics. $\endgroup$
    – Bbrk24
    May 16 at 20:01
  • $\begingroup$ @Bbrk24 Oh true, will add $\endgroup$ May 16 at 20:01
  • 2
    $\begingroup$ RPN is also an option, e.g. int list in OCaml. $\endgroup$
    – kaya3
    May 16 at 20:25
  • $\begingroup$ You can allow/require parentheses when nesting: Map (Thing String Int) (Option (Box Char)) $\endgroup$
    – xigoi
    Jul 19 at 7:38
  • $\begingroup$ @xigoi That's an answer already (and my personal favorite :p) $\endgroup$ Jul 19 at 14:15
7
$\begingroup$

Specialised syntax

Like Haskell, you could use specialised syntax for specific kinds of types. For example: (A -> B) for a function that receives an argument of type A and returns a value of type B, [A] for a list of type A, (A, B) for a tuple of an A and a B. And there are other possibilities, like {A} for sets of A and {A: B} for dictionaries with A keys and B values.

Pro:

  • These tend to make commonly used types less verbose. Instead of something like Func<List<A>, Func<List<B>, List<Tuple<A, B>>>> you would write [A] -> [B] -> [(A, B)].

  • It can visually couple expressions and their types. For example, say [42] has type [Int], and (True, False) has type (Bool, Bool)

Con:

  • This requires a more complex grammar for every kind that has a special syntax.

  • User-defined types need to still have their own syntax, so you will still need one of the other answers here.

$\endgroup$
1
  • 1
    $\begingroup$ You do need to be careful how you “visually couple expressions and their types” though, since it’s misleading if things look the same but have slightly different meanings. It often confuses beginners to Haskell that the type [Int] means a list of any number of integers (meaning [] Int) while the pattern [xs] matches a list of only one element, confusingly named xs (meaning (:) xs []). $\endgroup$
    – Jon Purdy
    Jul 22 at 4:55
6
$\begingroup$

A pairing that you're willing to give up on using for any other purpose

Angle brackets have a strong history in this role, but < and > are also common comparison operators. Square brackets are used in a number of languages, but often overlap with an indexing operator. So are parentheses, which often have overlapping uses for function application or precedence. Braces don't come up a lot for this use, but are heavily used elsewhere for scoping and collection literals.

These overlapping uses create parsing difficulties, particularly for partially-complete code that needs syntax highlighting. At what point do you know whether x < y * z is a type expression or not? Is list[string] *z declaring a pointer to a list of strings or multiplying a value? The language implementation itself probably knows, or knows what error to throw, but other systems need to parse the code as well, as do people.

Not every language has all of those complications - perhaps all comparisons are done with methods, indexing uses parentheses, application uses juxtaposition, etc. If there's a "keyboard pair" that isn't in use elsewhere, use that one, and if there's more than one, pick your favourite.

What if they're all gone?

If you do need to use angle brackets, parentheses, square brackets, and braces all for other purposes, doubling up is a good option. list[[string]] is a little more typing, but if [[ never appears anywhere else in code then you've immediately got a clearer understanding of what's going on with only a local view. Find a pair that can't ever show up in normal code and use that.

Alternatively, shift one of the other uses to a double if that is less common. If indexing is expected to be rare but types are ubiquitous, maybe it doesn't need to squat on prime real estate.

$\endgroup$
5
  • 1
    $\begingroup$ It's usually clear from context what's a type and what's a value, depending on the language's syntax, so I don't think this is a huge concern. Worth considering as a factor tho yeah (I don't think [[/]] would fix anything tho, still ambiguous with a multidimensional array or array in an index) $\endgroup$ May 16 at 22:10
  • $\begingroup$ Depends on the language. C and many of its relatives use braces for arrays, not square brackets, so [[ was illegal. Modern C uses it for some attributes though, like [[noreturn]], precisely because it was impossible before. $\endgroup$
    – Bbrk24
    May 16 at 22:12
  • $\begingroup$ @RadvylfPrograms That is language-dependent, of course, but for partially-complete code & to a machine rather than a person I don't think that's generally true. For example, x < y could be a side-effecting call to a user-defined operator < or the start of a variable declaration, and the parsing & highlighting you want on y is likely very different. $\endgroup$
    – Michael Homer
    May 16 at 22:16
  • 1
    $\begingroup$ If you have multidimensional/nested array literals where [[ can appear then of course you'd choose a different pairing - the point is that there's bound to be something unambiguous available. $\endgroup$
    – Michael Homer
    May 16 at 22:18
  • 1
    $\begingroup$ Coming back to this way late, but the first prototype of Objective-C used {| ... |} for method calls (where modern Objective-C uses [ ... ]) because that was illegal in C. $\endgroup$
    – Bbrk24
    Jun 26 at 22:47
4
$\begingroup$

Lisp-style

Another option which would work with any choice of brackets is a more lisp-style syntax: (Map String Int), (Option (Box char)), etc. One advantage of this would be that it's more visually distinct from function application.

Omitting the outermost parens feels like the most optimal mix of systems to me. Here are some examples:

  • String
  • Option String
  • Option (Box String)
  • Map String int
  • Map String (Option int)
  • Rc (Map String int)

One disadvantage would be that if your language has tuple types, this would be ambiguous without extra clutter.


Bonus wackiness: You could apply some sort of macro to a generic type using this syntax. E.g., if DBL were a macro that used a generic param twice, ((DBL Map) (Box (Option (Rc (Option char))))) would be a shorthand for (Map (Box (Option (Rc (Option char)))) (Box (Option (Rc (Option char)))))

$\endgroup$
1
  • 1
    $\begingroup$ The Lisp-style with outside parenthises omitted is, I believe, the same as what you get if you remove the specialized forms from Haskell's type syntax $\endgroup$
    – Potato44
    Jun 23 at 8:39
3
$\begingroup$

Ada's approach to generic programming consists in having a generic block before the procedure being declared:

generic
  type Element_T is private;  -- Generic formal type parameter
procedure Swap (X, Y : in out Element_T);

It binds Element_T to a type name locally, making the whole syntax quite uniform.

See Generics.

$\endgroup$
3
$\begingroup$

Keyword, and then parentheses

Borrowing from Zig's comptime, how about a comptype keyword, to annotate that some names are really syntax for an compile time only type name.

// Declaration

class HashMap comptype K, V
{
    public HashMap() {}
    public put( K key, V item ) {}
}

// Use

var map = new HashMap(String, i64);
map.put( "64", 64 );
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .