12 ms·
A more practical reason as a language designer is that C and C++-style type-before-name syntax is a nightmare to lex and parse, as you can't tell whether A *
by TazeTSchnitzel 7y ago
A more practical reason as a language designer is that C and C++-style type-before-name syntax is a nightmare to lex and parse, as you can't tell whether
A * B;
is a multiplication or a variable declaration, or whether
A<B, C> D;
is a two comparisons with a comma operator or a templated variable declaration, without first knowing the names of all declared types.
This means in practice that you have to declare types before they are used in a file, which means forward-declarations if they are defined later, that you can't separate lexing and parsing because the parser has to provide constant feedback to the lexer, and that misspelling a type name can lead to a syntax error! C++, not content with merely inheriting C's problems, throws in the “most vexing parse” as a bonus.
- Animats 7y agoIt's a historical problem in C. Originally, C had only built-in types. Structs were declared with struct foo { int x; int y; }; which is easy to parse. Then came typedef. With user-defined types, foo*bar; isn't parseable with a LALR(1) parser until you've seen the definition of "foo". Even the ordinary case foo bar; needs more than one lookahead to parse. This is a headache for compilers, and a huge headache for anything that wants to work on single source files without seeing the included files. It's a big win if files are parseable without their dependencies. The Pascal/Modula/Ada family all are. I think Go is. Not sure about Rust, but it probably is. C and C++, no. (The hacks for template syntax in C++ are painful to think about.)
- TazeTSchnitzel 7y agoAh, C not originally having typedefs explains why the syntax wasn't thrown out as too annoying to begin with. Syntax creep I guess. An easy solution to the problem would be to do what e.g. Haskell does where case-sensitivity determines a type name. For better or worse, C didn't do that.
- lisper 7y agoIn Dylan all types were delimited with <angle_brackets>. Personally I always found that very aesthetically pleasing.
- Riverheart 7y agoSame in Powershell except they use square brackets [string] $foo = "foo"
- zelphirkalt 7y agoIn my brain the instantly translates to "list of strings". Not sure square brackets are such a good choice.
- lmkg 7y ago> Not sure about Rust Rust has made a point of being parser-friendly, to support tooling. The entire reason the turbofish ::<> exists is to avoid ambiguous grammar. (This operator seems to vex people, I don't get the hate.)
- estebank 7y agoDo note that Rust the language is regular and easy to parse, but rustc the compiler actually performs bounded lookahead and contextual parsing on parse errors to detect common typos and invalid syntax that would otherwise be vexing to users. The nightly only type ascription : syntax is a good example because it crops up in several different contexts as a 1-substitution typo and used to be terrible (may it burn in a fire). The turbofish is another case where we now suggest the correct syntax, but there are many, many more.
- usrusr 7y agoSurely that's an example of how parser-friendly syntax empowers tooling? I imagine that it would be much harder to guess intention and fixes from lookahead and context if those were already necessary to make sense of correct code?
- WalterBright 7y agoBeing parser-friendly is not at all the same thing as being user-friendly. The difficulty of writing parsers is way overblown. One thing to avoid is depending on the existence of the symbol table to parse correctly. (C++ has this problem.) Needing a symbol table makes it hard for parsers like source code formatting and syntax highlighting. Another is to avoid raw string literals that reach back and try to unwind the results of previous "phases of translation".
- barrkel 7y agoAgree with a caveat; if the syntax has many ambiguities, with resolution solved by a clever parser, it'll be confusing for users, especially newbies, because they'll tend to get errors unrelated to what they were trying to achive. Redundancy in syntax can be a bonus.
- barrkel 7y agoYeah, the easiest way to do it is return a different token type from the lexer for type names, which means the lexer looks up the symbol table when it sees identifiers. Amusingly, the Delphi dialect of Pascal has an ambiguity in its grammar from how it made function pointers work: function g: Integer; // ... f(g); Depending on the definition of f, this could be passing the function f by reference (as a function pointer), or passing the result of calling f. Delphi looks at the argument type to resolve the ambiguity, but it stays awkward in overload scenarios, when there's a choice between Integer or function pointer arguments. At some point around about 2009, I added the ability to specify explicitly that you want the call, to eliminate the ambiguity: f(g());
- yaktubi 7y agoOn the other hand doesn’t forward declaration make sense? The code is declaring it depends on the forward declaration defined “somewhere else”. In a sense it is documenting the the classes/structs better.
- BruceEel 7y agoYears ago, I stumbled upon a Visual C++ 6.0 bug (if memory serves me right), just by playing around trying to understand C/C++ decls. It would crash on a stray: *c; at the beginning of a translation unit. Didn't K&R have a tiny, tiny C declarations parser (printing out 'human readable' equivalents) example in their book? I think the caveat was that it needs to assume it's dealing with a declaration...
- stormbrew 7y agoIt's possible to argue here that the problem is not so much the order of type-then-name but the overloaded meanings of * and < at the grammar level.
- lilott8 7y agoMore generally this falls into a discussion of context-specific vs context-free grammars. Of which C++ falls into the former, Java falls into the latter.
- MaxBarraclough 7y agoThe grammar of C++ is not a context-sensitive grammar. It's Turing-complete, on account of its template metaprogramming capabilities. I'd be very surprised if Java's grammar were context-free. Do you have a source for this? I wasn't able to find one with a quick search.
- battery_cowboy 7y agoIt's Turing complete because it could simulate a Turing machine, not because of metaprogramming. The language brainfuck is Turing complete, for example.
- saagarjha 7y agoChecking if a Brainfuck program is well formed (i.e. can be run) is a linear time operation. In C++ this can take forever. They have different complexities.
- battery_cowboy 7y agoThe original comment was about Turing completeness, and it was defined incorrectly. I was giving an example of a dead-simple language that was Turing complete, because the claim was that metaprogramming made C++ Turing complete.
- Groxx 7y ago`int * A` and `A * int` both seem ambiguous unless a) you require "int-pointer" naming instead of "pointer-int", and/or b) you have a separator (like the title uses) so this becomes `A * B` vs `A: * B` which is indeed unambiguous but in a very different way. i.e. without extra rules you can't tell either way, so it comes down to the extra rules. `A * B` is unambiguous for type-before-name if you require "pointer-int" since it would need to be `* A B` for A to be a type. (quite possibly there are counter-examples when you get into weirder corners, but my point is that it's not as simple as presented)
- WalterBright 7y agoD resolves the: A * B; // declaration or multiplication? in an unusual way. If it was a multiplication, the result would be thrown away and be pointless. Hence, it is a declaration. But what if A overloads the * operator and the side effects are desired rather than the result? D has a philosophy that arithmetic overloads should be for arithmetic-like operations, not I/O, template metaprogramming, or other nonsense. Hence if you try it like that, too bad, so sad, it'll be treated as a declaration.
- Ididntdothis 7y agoI wouldn’t mind if such a statement in isolation would be a compiler error and if languages in general were much stricter. So far all languages I have seen that are very forgiving in terms of syntax (PHP comes to mind) seem to breed sloppy programmers and software written in these often have stupid little bugs caused by typos.
- WalterBright 7y agoThis particular issue has not caused any "silly typo disease" problems that I'm aware of in D. In fact, pretty much nobody notices it, it just works the way people expect it to. (There are some other things in D that are designed to discourage trying to overload arithmetic operators for non-arithmetic purposes. For example, < <= > >= cannot be overloaded individually, only as a group.)
- downerending 7y agoArguably a programming language is ultimately a user interface, and the more intuitive the interface, the better. In 2020, not sure we should care that much about how hard compilers have to work to achieve this. Computers and software are here to support us--we're not here to support them.
- MaxBarraclough 7y ago> In 2020, not sure we should care that much about how hard compilers have to work to achieve this. That's not right. Long compile times are a real issue for some programming languages even today (Rust and C++). > Computers and software are here to support us--we're not here to support them. A false dichotomy, and not a perspective that offers any insight. Sometimes a low-level language is appropriate, and sometimes it is not.
- Gibbon1 7y agoC++ is stuck in this trap where because it's slow to compile, the compiler maintainers increase the amount of optimizations the compiler does to make it faster. Which of course makes the compiler even slower. Which motivates them to increase the amount of optimizations. Which makes the compiler yet slower. I feel part of the problem with rust is it doesn't have quick and dirty mode thats fast and a production ready mode that is slow but does all the checks. I do this with my C programs, using various formal analysis tools which are slow to vet the code before releasing it.
- steveklabnik 7y agoThe checks are not the most expensive part of compiling Rust.
- Gibbon1 7y agoWhat is it then?
- 7y ago
- MaxBarraclough 7y agoI missed your mention of the 'most vexing parse' before writing this up, but for those not in the know, here's another fun quirk of C++ syntax: Declare a local named neko of type Kitten, passing a value to its constructor: Kitten neko(42); Declare a local named neko of type Kitten, without passing a value to its constructor: Kitten neko; Declare a function named neko with zero parameters and with return type Kitten: Kitten neko(); https://en.wikipedia.org/wiki/Most_vexing_parse https://en.wikipedia.org/wiki/Most_vexing_parse , https://stackoverflow.com/a/620149/ https://stackoverflow.com/a/620149/
- a1369209993 7y agoAnd Ceiling Cat help you if you try to declare something like: Kitten neko(Felis<0xCA7>::catus); where catus may be either a typename or a variable depending which template instantiation you landed on. There's probably some way to say: Kitten neko(Felis<is_function_type<typeof neko>::value_of>::catus);
- MaxBarraclough 7y agoBeautifully terrible!
- mariodiana 7y agoI was just thinking about pointer syntax today, and if you ask me, there are a lot of problems that could be avoided if language designers took a page out of Guido van Rossum's book and extend his idea of forced spacing. Take the first example you've given, and let's just talk about variable declaration: int* a; int * a; int *a; That's all the same. That's wrong. Obviously, you can write a lexer and parser that doesn't give a crap, but the human mind does. If you think it doesn't, that's only because you've internalized the various cases. It should be this: int* a; The type we're talking about is a pointer to a variable of type int: in other words, "a" is an int-pointer. If you were to create a macro, you'd do something like this: #define int_ptr int* Do you see what I'm getting at? The star in this case is a suffix, equivalent (in our minds) to "_ptr". Conceptually, it doesn't belong anywhere else than attached to the type. It's a compound type, conceptually. Now, take the star being used in a different context: int b = 10; int* a = &b; printf("%d\n", *a); There, though we see the same character, it's a completely different thing. It's a dereference operator. Conceptually, it belongs attached to the pointer variable it is dereferencing; and since the star is already used in one context as a suffix, here it should be used as a prefix. This is no good: printf("%d\n", * a); It doesn't matter that "this compiles." That's not what this is about. Many C programmers (and programmers in other languages, even Python) are used to writing things like this: int c = x*y; That's wrong. Sure, the lexer and parser don't care. But that makes the language worse, for the human operator. "But it saves space!" Spare me. The thing with this one example, using the star, is that what we have is the equivalent of a homonym. We have one sign that is actually three different words. Mandating spacing removes the ambiguity you're complaining about. C is what it is, but if we imagine someone were going to write it today, they should incorporate the above and mandate spacing. For the sake of the humans. "ident type" is not the only solution.
- klyrs 7y ago> It should be this: > int* a; I must disagree... the following sends the wrong message the reader: int* a, b; Though I also agree that dereferencing is best without a space. Perhaps the correct suggestion is to not declare pointers and instances on the same line, but that's a convenience that many seem to enjoy
- threatofrain 7y agoMakes me wonder why Dart went the same way. One thing I don’t like about type first is that what you want is often more specific than the type.
- WalterBright 7y agoIronically the article chose the < > syntax: class Id<T>() { Instead of the D syntax: class Id(T) { Not using < > for template parameters eliminates all kinds of parsing problems.
- ZoomZoomZoom 7y agoI don't see it being ironic. In Rust () is always a tuple, so if you see it that way it makes sense () isn't used for type declaration.
- pierrebai 7y agoThis has nothing to do with order and everything to do with the chosen grammar of C and C++. You can have an ambiguous grammar in a language that puts the type after. A * B could be ambiguous even if A is the identifier name and B the type. All arguments in the "Language Design" notes are of the same nature.
- raverbashing 7y agoI think this C pointer ambiguity would be "easy" to solve with one of those minor changes: 1 - (sorry, formatting is messing this up) mandate the declaration: 'A*' or '*B' without space (depends if you think the pointer is a type of variable or a type of type) 2 - add some keyword like "type" Though the compilers are able to deal with it, so it's not a big issue