IMO there’s several interlocking reasons here. Type inference is the killer feature, but type inference is also usually desired and implemented by people who have some background in ML/Haskell, so they use the syntax they are familiar. I suspect they are also a bit more willing to break conventions to get things done.
Types on the right are also easier to parse, especially when the type is complex. Doing int *foo; is technically ambiguous but still pretty easy to parse consistently, but when you have more complicated types like Map<Warehouse, List<OrderItem>> items then getting the generic parsing right is More Fiddly. Knowing that your parsing rule starts with something simple like an identifier, or even better a keyword like var or let, means there’s a lot less room in your syntax for things to randomly explode. Java didn’t start out with these sorts of complicated types in the early 90’s so it wasn’t much of a concern, so they just went with what looked like C. But by the early 2000’s C++ was more pervasive than in the 1990’s, and people wanted some of its features without its well-known parsing headaches. So D’s templates didn’t use anything like C++‘s template syntax, for example, while C# decided “looking mainstream” was important enough to go through the extra effort. (That bit is my guess, I’d love to know more details.)
Map<Warehouse, List<OrderItem>> items
We’re slowly (finally) evolving away from “looks like C” being a requirement for a mainstream language, so when Go had an opportunity to both simplify things and have complex nested types they chose a type syntax that was much simpler and more consistent than C’s. Scala and Rust did the same and became reasonably mainstream, and now others have the courage to pick up the trend because it won’t immediately label their language as Some Weird Thing.
TL;DR: In addition to what the article states, there’s some minor technical reasons types on the right is better, and the people who are writing these new languages are familiar with them and more willing to break the mold. I could go on, but I probably shouldn’t; suffice to say that there’s similar reasons why Rust’s .await is a postfix operator.
I can agree to the variable declaration parsing simplification as long as you begin with var, but honestly then you could just have:
var <type> <name>, it the initial entry token that simplifies it - however this wouldn’;t look as nice as the Pascal syntax which I have a sweet spot for, so you do bring atleast one very objective point into the parsing domain with your argument sir.
var <type> <name>
Many a lookahead may be averted with such a var technique.
Hmm, I never considered var <type> <name> and I’m not really sure why not. Maybe with type inference where you can have <type> <name> or just <name> it still risks ambiguity? Though you have the var token so you know you’re in a var decl already, so it should work fine. Weird.
I mean it could possibly solve both camps, C devs just prepending var and then Pascal devs just swapping around and dropping the :.
In case of C/C++ it’s not simply the type on the left, but a declaration that may have the name somewhere in the middle according to the spiral rule (which is an approximation not even a rule). This becomes apparent when you use function pointer types.
Or arrays, as in
char foo = "abc";
Which leads to more complex rules to construct a type expression without an associated name, as needed in casts, etc. It was a botched design decision from the beginning, but due to the incredible conservatism of “all languages must look like C now”, it has taken many decades to get past this. C is now 50 years old from the first working compiler.
Personally always found types on the right harder to read. Easier to parse for the computer maybe, but not necessarily for the human. Maybe it’s that there’s something between the variable name and its value: (…) x = 1 seems easier to understand than x (…) = 1.
If you have something like:
std::unordered_map<std::string, std::pair<std::string, std::vector<std::string>>> data = generate_data(some_long_expression);
it’s hard to even find the variable name.
That’s why languages have added type inference, so you basically never write complex types for variables.
Rust also supports partial inference when you need to hint some of the type, but not all, e.g.
let map: HashMap<_, _> = pairs.collect();
It’s not even an addition, it’s just taking advantage of what compilers were already doing from the start. Say we have this declaration:
var x : <type> = <expr>
The compiler needs to make sure type of <expr> matches <type>. What do they do most of the time? They infer the damn type first, and then they compare it. Language that support omitting the type merely skip the second part, and the type of x becomes whatever was inferred first.
Well, for local inference at least. Inferring the type of functions is a bit more involved, and does count as an addition when it’s implemented.
That’s for C++ auto. There are various levels of inference, even within functions. For example:
let x = Vec::new();
The first line does not know the element type, but with a Hindley–Milner type inference the type on the first line can be deduced from the second line.
I have never declared such a monstrosity and if I had to then it’d be auto’d, I could argue against the clarity or rather lack-therefof resulting in clutching to inference.
I’d argue that C++ is good at adding noise, too. HashMap<String, (String, Vec<String>)> is worse than it needs to be, but not too monstrous. I think in Haskell it would look something like Map String (String, [String]) ?
HashMap<String, (String, Vec<String>)>
Map String (String, [String])
But how could you possibly know what String it is if you don’t have a namespace telling you? /s
Like the OP, I like having my variable names line up. That promotes readability.
Haskell does this (lets you align your names in the same column), without doing the thing you don’t like, putting a large type expression between the name and the =. Instead, you put the declaration on a separate line from the definition. Types are still on the right.
f :: Num a => a -> a -> a
f x y = x + y
Maybe I am just rationalizing something, I am used to, but I do not see enough reasons for the change.
int abc = 123;
--- --- almost never
When I am interested in the type and the name, I read int abc without being interrupted by any irrelevant information.
And when I am interested in the name and the value, I read abc = 123 also without being interrupted. I am almost never interested in just the type and the value.
abc = 123
If there is var or auto keyword it is just a wild card placeholder for a type (that is not explicitly named in the code). Having a var/auto and an explicit type for a variable, does not make much sense for me.
This trend saddens me. As a D user, you can have my left-hand types when you take them from my cold dead hands.
From my cold dead hands :sunglasses_emoji: pew pew.
Also a D enjoyer here, not a Pascal trend enjoyer although I enjoy messing with Pascal now and then I much prefer aligned types (textually) and in general I am not an inference fan.
Blog post kind of misses the true driver here, which is that it is significantly harder to unambiguously parse types-on-the-left in the presence of increasingly complex user-defined type syntaxes.
C++ is already way too far out over its skis in this regard, and was so before it really got type inference. So the correlated rise of type inference is actually a bit tangential to the main driving factor.
way too far out over its skis
way too far out over its skis
Excellent metaphor; gotta work that into conversation.
When golang came out, their rationale was that we think in that order rather than type first: ‚I’m going to create a which will need to be type with value ‘.
It seems reasonable (and I’m comfortable with it now) but at the same time, I find myself often writing types out before the actual implementation and use of them, so maybe that’s a point for the other team
I am fine with that trend. Something that always throws me off though is the colon that some languages add. It feels kind of unnecessary to me. A rationale I heard is so it’s clear which side is the type, but I don’t really understand how it is. Wouldn’t the same thing be true with a space? Another reason that I heard is parsing. Maybe I am missing something, but why would that be?
What I mean with “throws me off” is that this character doesn’t seem to serve a purpose, and it’s something that when you type a lot feels “hard” given it is yet another thing that you can typo on, needs shift on most layouts, etc. It’s not that much of a deal, but I notice how this is a trend as well and I am curious about why this is.
I know that some IDEs add a colon, but I thought it was a good trend that language became less verbose, so you don’t need an IDE to write half of the code so to speak.
In Ocaml, the extra column actually does something: any expression there can be annotated with a type. Sure, at the declaration end, it serves no purpose:
let x : int = 2 + 2
in x - 1
But you can also do this on the right hand side:
let x = (2 : int) + 2
in x - 1
The reason this changes everything is because the a b syntax is already taken by function calls. See, ML noticed that the one thing you do the most is call functions. So it should have the lightest possible syntax, which is, no syntax at all. In this case, if you remove the colon, how would you distinguish (f x) from (2 int)? That’s not something you can know at parsing time. At least not without a look-up table like we use when parsing C and C++.
Then I guess this colon were ported to other languages that don’t have this ambiguity (they write f(x)), out of habit. Because in the end, the variable : type syntax is pretty widespread among those who actually studied types.
variable : type
The colon is from the math syntax afaict, looking up simply typed lambda calculus and such tends to involve an expression e: T which says “expression e has the type T”.
You’re right that it’s probably not strictly necessary.
This post converted me from right-wing to left-wing very quickly.