17
$\begingroup$

Famously, C’s functions and variables have their types specified before the function/variable name:

bar foo() {...}
bar foo = ...;

Most older C derivatives also use the prefix syntax (e.x. Java, C#, C++). However, many newer languages (e.x. Rust, Kotlin, Typescript) use a postfix type syntax:

function foo(): bar {...}
var foo: bar = ...;

What are the advantages of the former syntax over the latter?

$\endgroup$
7
  • 2
    $\begingroup$ Exact duplicate of a question on CS.SE. Posting questions that already have an answer on Stack Exchange is not a good way to populate a private beta: it suggests that the new site doesn't have its own niche. $\endgroup$ May 20, 2023 at 20:44
  • $\begingroup$ also related (in relation to "cross-posting"): meta.stackexchange.com/q/64068/997587 and meta.stackexchange.com/q/354865/997587 $\endgroup$ May 20, 2023 at 21:07
  • 2
    $\begingroup$ @Gilles'SO-stopbeingevil' 1. I did not realize this, than you for pointing out. 2. we have a bit of overlap with CS 3. This is still on topic, no? $\endgroup$
    – Seggan
    May 20, 2023 at 23:48
  • 2
    $\begingroup$ The trailing : type syntax is usually called "pascal" or "modula" syntax, since that is where it originated. This was ~1970, so actually predates C by a little bit. $\endgroup$
    – Chris Dodd
    Aug 20, 2023 at 1:13
  • 1
    $\begingroup$ C++ actually has both prefix and postfix return types. $\endgroup$ Aug 20, 2023 at 11:38

8 Answers 8

15
$\begingroup$

Consider this line of C:

foo(*bar);

This could be:

  1. A declaration of a variable named bar, which is of type foo*.
  2. A call to a function named foo, passing an argument which is the the pointer bar dereferenced.

The way that the C grammar handles this is by distinguishing between type names and other identifiers. Essentially, on every typedef, the symbol table is updated to say that this identifier is now a type, and the lexical analyser returns a different token.

One disadvantage of this is obvious: a language with this specific syntax quirk cannot use a type before it is declared, hence the need for forward declarations. It is now the 21st century, and a lot of programmers don't like this.

As an aside, Haskell has a similar problem with its type language, which is solved by requiring that type identifiers and type constructors start with an upper-case letter and variables start with a lower-case letter. Prolog distinguishes between variables and atoms/functors using the same approach. I digress.

Wirth-style syntax doesn't have this problem because type names and other identifiers don't appear in the same "location", such as at the start of a statement-or-declaration.

Moreover, Wirth-style languages typically have operator grammars, which mean that no two user-defined identifiers can appear next to each other without an operator or keyword separating them. As well as making the syntax less ambiguous, it can make syntax error recovery easier.

The cost is a very slight increase in verbosity. Wirth syntax requires a colon where C-style syntax requires only whitespace.

But on the other hand, Wirth-style languages don't pay for verbosity in places where C-style languages do, such as the parentheses around conditionals or the parentheses after function calls with no arguments.

/* Look at those parentheses. Can you see why they are
   necessary? */
if (boolean_variable) procedure_call();

{- The Pascal equivalent may superficially feel more
   verbose, but it's only one character longer. -}
if boolean_variable then procedure_call;
$\endgroup$
1
  • 2
    $\begingroup$ See also en.wikipedia.org/wiki/Most_vexing_parse. I recall reading a paper about making changes to C++ to make the grammar context free for example by using the syntax "x:int". Alas I cannot find the link. C++ is actually moving in this direction slowly. We now have trailing return syntax for example: auto X() -> Y (X is a function returning Y). $\endgroup$ May 30, 2023 at 16:56
12
$\begingroup$

The main advantage is that the syntax is shorter ─ one less keyword function, and one less character of punctuation :. A secondary reason is that this is consistent with how variables are declared, type first, in those languages ─ but this really just expands the question to be about variables as well as functions.

The main disadvantage with the type-first syntax is that it makes it harder for type annotations to be optional. Modern languages may have more expressive type systems where types such as {x: T['bar'], y: readonly number[]} might be noisy to write (and might not have a name), but they also run on modern hardware which can afford to spend computational resources on type inference, so that the compiler can know what the type is without the programmer having to write it out. Additionally, languages like Python and Typescript are "gradually typed", meaning that even where a type can't be inferred, the type annotation can be omitted and the program still makes sense (with a fallback to dynamic typing). So a syntax like var x: Type = value; allows the : Type part to be omitted without needing something else in its place.

Other than that, the use of punctuation like : to indicate type annotations has benefits for languages like Typescript where the syntax for types is widely varied; a type in Typescript can begin with a letter, underscore, digit, single or double quote, backtick, parenthesis, square bracket, or brace character. By requiring a colon at the start of each type annotation, the parser (or the human reader) doesn't have to guess whether the line {x: number} foo = {x: 4}; starts with a type annotation or something else. Not to mention that in some cases this would genuinely make the grammar ambiguous.

It's worth adding that the notation $x : T$, with a colon rather than some other symbol, is the standard notation used by type theorists for the proposition that a term $x$ has type $T$.

$\endgroup$
3
  • $\begingroup$ Type-following-declaration languages don't necessarily need a keyword to introduce a function definition. You can use a grammar rule like function-definition : identifier '(' argument-list ')' ':' type-description function-body. It needs one more symbol of lookahead to parse if you're working left-to-right, but very few languages are LL(0) so you likely need this for other parts of your language anyway, so it isn't a big deal. $\endgroup$
    – occipita
    Jul 21, 2023 at 0:46
  • $\begingroup$ @occipita It needs more than one token of lookahead, because identifier ( argument-list ) is also valid syntax for a function call, so you won't know it's a function definition until you see a : token either in or after the argument list. My language Papyri used to be like that, but it made parsing complicated, it made it complicated to read code as a human because function definitions looked too much like function calls, it made error messages confusing, and also if every function definition needs at least one : to be recognised as such then type annotations can't be optional. $\endgroup$
    – kaya3
    Jul 24, 2023 at 9:48
  • $\begingroup$ kaya3: in my design functions cannot be defined in locations where expressions would also be accepted. Therefore a function definition only needs to be distinguished from a variable declaration, an import statement, and one of several forms of type declaration (all of which are prefixed by a keyword). I don't support nested named functions -- only lambdas can be nested. If you really want to define a named function locally you need to store a lambda in a variable (which is easy: id := \ x -> x declares the identity function). $\endgroup$
    – occipita
    Jul 24, 2023 at 15:34
8
$\begingroup$

It has to do with syntactic ambiguity. In most languages, we have a different syntax for types and terms. For example, when parsing a type, < is paired with > and means generics, while in terms, < and > are comparisons.

Now, let's say we want to parse a statement. When we see a couple of tokens, a, <, b, >, c, we do not know if this is an expression (a < b) > c or if this is a variable definition a<b> c. Then, if there's a parse error, the compiler would expect things that either completes a definition or an expression, which can be hard to understand. With postfix-type syntax this is not even a problem.

$\endgroup$
6
$\begingroup$

This is largely an arbitrary choice. One language did it some way, a long time ago, and then many but not all other languages copied a familiar syntax. That being said, the choice is not completely arbitrary. Putting the type before or after the variable name is largely cosmetic, but there are more differences than this between the C-style syntax and the Pascal-style syntax used in languages like Rust and Scala.

In early languages where variable declarations had a type, the syntax of a declaration was of the form Type Name where Type is a reserved word and Name is a name chosen by the programmer. Here the order is not arbitrary: first you say what kind of thing you're declaring, then you give it a name. Compilers at the time were very primitive and needed to know early on what they were parsing; writing INTEGER X let the compiler know immediately that it had to work on an integer declaration, whereas X INTEGER would have required the compiler to first remember that it had seen X and only after that it would have known that it was supposed to declare an integer variable. Languages of the 1950s and 1960s such as ALGOL and FORTRAN tended to use this syntax. Interestingly, at the time, in programming language descriptions, “type” had a more general meaning that it has today: today it has a specific technical meaning that's more or less synonym with “datatype”, but at the time it had its ordinary English meaning, which could cover things like integer and real, but also things like label and statement.

C extended this syntax in a way that allowed defining compound types. In C, the syntax of a variable declaration mimics the syntax of its use. For example, int x declares an integer called x, and int *p declares a pointer-to-integer called p, and the expression *p has the type int. This syntax is not purely prefix because accessors can be suffixes: for example a[1] is an int if a is an array of int values, so the syntax to declare an array of integers is int a[].

C became a very popular language, so many languages after it retained a similar syntax for several things including variable declarations and types.

The colon syntax goes back to at least Pascal in 1971. This is the syntax used by computer scientists at least since the early 1970s. I suspect that it started in theory papers and was adopted by Pascal and later by other languages, but for all I know it may have originated with Pascal. Why colon to denote that a value belongs to a type? discusses the origin of $x : T$ in computer science. Modern languages that draw from modern (or not-so-modern-anymore) theory tend to use the syntax that's familiar to computer scientists, including ML, Scala, Swift, Rust, and many others. Haskell is somewhat unusual in using a double colon :: where others use a colon :.

The order $x : T$ vs $T : x$ is arbitrary: these days, compilers don't mind, and for humans, it's just a matter of habit. However, having a punctuation sign between the variable name and the type is not arbitrary. Most languages that use this syntax also allow the left-hand side to be an expression, to form a type annotation. More importantly, the right-hand side is a type expression, not just a type name. Recall that C supports declarations of variables with compound types, but the syntax is somewhat awkward, with the variable name ending up somewhere in the middle. C's syntax does not work with compound types that are made of more than one type part. For example, in ML, x : bool * int * string means that x is a triple consisting of a boolean, an integer and a string. Haskell writes this x :: (Bool, Integer, String). Both the ML and the Haskell way allow building complex types. The C approach fails: where do you put the variable name?

Languages such as Java and C# deviate from the C syntax by putting the whole type information before the variable name: int[] a, (bool, int, string) x, List<int> v. With no separator, it's impractical to allow both sides to have an arbitrary length, but these languages do support type expressions, which can be used to declare a variable.

(reposted from https://cs.stackexchange.com/questions/109817/in-function-declarations-what-advantages-are-there-to-having-the-type-of-the-pa/109978#109978)

$\endgroup$
2
  • $\begingroup$ Can't C represent anonymous product types like struct { bool first; int second; char* third; } x;? $\endgroup$
    – Bbrk24
    May 20, 2023 at 20:56
  • $\begingroup$ @Bbrk24 Yes, and those deviate from the idea that the declaration syntax matches the use syntax, because that becomes very clumsy when a type has multiple fundamental destructors: you can't write “the type such that t.first is a bool and t.second is an int” without repeating the type name t, which would be a lot of work for everybody and thankfully C didn't go that far. $\endgroup$ May 20, 2023 at 21:12
5
$\begingroup$

One reason that C++ introduced trailing return type syntax is that this allows the return type to use dependent names. That is, names that represent types only with the greater knowledge obtained when reaching this position, usually because they only name types in the context of an argument, often the implicit this argument of a member function.

Not being able to do that can be seen as a disadvantage of prefix position for return types.

$\endgroup$
4
$\begingroup$

Disadvantage: Making the variable name harder to find between the type syntax and value

Having the type before the name can make it hard to find the name, which is the most important part of a declaration. Consider a line like this:

std::pair<std::unordered_map<int, std::vector<double*>>, std::unique_ptr<unsigned long long>> const frobnicator = std::make_pair(create_frobni(), create_cator());

How long does it take you to find the name? Or even to recognize that this is a variable declaration?

$\endgroup$
1
  • 1
    $\begingroup$ This is less of an issue with the ordering and more of an issue with the verbose type names. The same type in Swift might be called ([Int32: [UnsafeMutablePointer<Double>]], Unique<UInt64>) which, besides the spelling of double*, is a lot shorter. (I'll add that there is no standard equivalent to std::unique_ptr, so I'm assuming a user-defined Unique<T> type.) $\endgroup$
    – Bbrk24
    Jul 19, 2023 at 14:29
3
$\begingroup$

In addition to what has been said about the relative position of declared value and its type, the prefix/postfix notation also comes up in how types themselves are denoted. In ML, types are in postfix but type parameters in prefix position:

val assoc : 'a -> ('a * 'b) list -> 'b

The value (a function) declared is assoc and its second parameter is a list of tuples ('a * 'b). Notice that the type parameters for list are in prefix position. Other examples would be int array or char list. array and list are formally type constructors: types that take parameters and produce a type.

Most other languages place type parameters in postfix position. This creates a symmetry between values and types:

  • A function takes value parameters and produces a value.
  • A type constructor takes type parameters and produces a type.

Treating types and values symmetrically makes it syntactically easier to support higher-kinded types.

$\endgroup$
5
  • $\begingroup$ I find this an odd use of language. When we write something like -5 we don't say "the operand 5 is in postfix position", we say "the unary operator - is in prefix position" or "unary - is a prefix operator". Likewise in 'a list, the type operator list is in postfix position, and list is a postfix type operator. The reason being that 'a is a type which can occur in various positions, but list is an operator which can only occur in one position relative to its operand. $\endgroup$
    – kaya3
    Aug 20, 2023 at 9:21
  • $\begingroup$ Didn't the question introduce the problem by saying "postfix type syntax" when referring to the type being to the right of the value? $\endgroup$ Aug 20, 2023 at 9:44
  • $\begingroup$ OK, I see the confusion. The question is talking about declarations, like int x; or void y() {} in C or Java, or let x: number; or function y(): void {} in Typescript. In those declarations, int, number and void are types, and they occur either before or after the identifier they declare the type of. But list is not really a type in ML ─ 'a list is a type, but list itself is not ─ and the term 'a list is not a declaration saying that 'a is the type of list, nor that list is the type of 'a $\endgroup$
    – kaya3
    Aug 20, 2023 at 10:17
  • 1
    $\begingroup$ Since list is an operator which takes an operand, it does make sense to talk about its fixity (i.e. the position of the operator relative to its operand, which is a type). But saying that "In ML ... type parameters are in prefix position" is strange and not really correct in general; in 'a -> 'b the type parameter 'b is not in "prefix position" in any sense, and likewise in 'a * 'b. Hence it only really makes sense to talk about the fixity of type operators like list (postfix), -> (infix) and * (infix), not about the fixity of types themselves. $\endgroup$
    – kaya3
    Aug 20, 2023 at 10:23
  • $\begingroup$ I see your point. I tried to say: ML has types declared after the value and uses postfix type constructors (which I think is a mistake). $\endgroup$ Aug 20, 2023 at 11:13
0
$\begingroup$

Advantage: Keeping the value and name close together which can increase readability

Prefix keeps the more important parts of a variable declaration (its name and initial value) together rather than separating them, making it easier to scan, i.e:

// Prefix:
███████ spug = calculate_spug(foo) + correct_error(bar);

// Postfix
spug ███████ = calculate_spug(foo) + correct_error(bar);

Note that there is also a second way of writing postfix types, used in some languages:

// Postfix after the value
var x = 7 as int;

which leaves the more important parts of line together and shares this advantage of code readability. However, this doesn't apply to the method given in the question.

$\endgroup$
4
  • 2
    $\begingroup$ This is entirely subjective, and without any hard evidence to suggest that one is more legible than the other I want to call it a matter of opinion. Besides that, most languages with postfix types still have a var keyword to declare a variable (PL/pgSQL being an exception off the top of my head). $\endgroup$
    – Bbrk24
    Jul 19, 2023 at 13:17
  • $\begingroup$ @Bbrk24: A var keyword just makes the advantage for prefix larger. I actually included that in the first draft but since there exist postfix languages without it, I think it is a tangential issue. As for subjectivity, I don't agree - the benefits of grouping for readability are well accepted - and it's certainly no less subjective a claim than xigoi's claim to the opposite. $\endgroup$ Jul 19, 2023 at 14:16
  • 1
    $\begingroup$ I am unsure why this answer received more negative attention than xigoi's. Both need more backing for their claims $\endgroup$
    – mousetail
    Jul 19, 2023 at 14:25
  • 1
    $\begingroup$ @mousetail I would argue that neither need backing for their claims; they're not necessarily saying "this is objectively more/less readable than that", they're saying "this is an objective statement about this type of syntax which could be a reason someone [typically the author] finds it less readable" $\endgroup$ Jul 19, 2023 at 14:37

You must log in to answer this question.

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