7 ms·
> The premier example of the adverse effects of cleverness in programming language design (and one which is obvious to programmers at all skill levels) must sur
by anyfoo 5y ago
> The premier example of the adverse effects of cleverness in programming language design (and one which is obvious to programmers at all skill levels) must surely be the C/C++ declaration syntax [10]. On the surface, it seems like an excellent notion: let declarations mirror usage. Unfortunately, the very concept undermines the principle of visually differentiating semantic difference.
Does it ever. From today's point of view, C's declaration syntax is just bizarre, especially if you spent some time with a "modern" type system[1]. The result of this declaration-mirrors-usage cleverness is not just the more encompassing insanity like the emergence of the "spiral rule", as found here: [2]. (Isn't it fun that that site promises to enable "any C programmer to parse in their head any C declaration!"? You know you have a problem when that's a novelty.)
It's also the emerging little paper cuts, like the fact that it should be "char* c" with the * hugging the char because * modifies the type and so is part of it, but because of the "mirror usage" concept you better actually write "char *c", because that's how the parser groups it, as part of the variable.
In effect, multiple declarations in one statement must have confused many novices, when they wrote something like "char* p, q", and surprisingly got a "char* p" and a "char q".
> Students have enough trouble mentally separating the concepts of declaration and usage, without the syntax conspiring to blur that crucial difference.
Yeah. I learned C in the 90s (and fortunately by now know exactly where to distribute my "volatile"s and "const"s in a complex type), but I still remember how confusing it was that the * practically did the opposite in declaration vs. usage. In a declaration it makes a pointer, in usage it dereferences it.
Consider the very important difference between those:
char *p = q;
*p = q;
Fun.
[1] "Modern" in quotes, because one prime example, Haskell, is old enough to appear in this 1996 article.
[2] http://c-faq.com/decl/spiral.anderson.html http://c-faq.com/decl/spiral.anderson.html
- abecedarius 5y agoThe bigger mistake was to mix a prefix pointer operator in with the postfix [i] and (x). You end up with the spiraling-out parsing even for expressions, not just declarations. With Pascal's postfix ^ everything reads left to right.
- deleted 5y ago[deleted]
- voidhorse 5y agoCouldn’t agree more. Every time I need to write/read C I’m forced to refer to some documentation just to recall what the right syntax is depending on whether or not I’m declaring or using pointers. I think go is a good example on how to turn “cleverness” into clear conventions, which usually yield the benefits of cleverness without the headaches. Semantic cleverness becomes problematic too at times. There’s a fine line between developing a useful abstraction and doing something so obtuse your users are scarcely able to discern what’s going on once they run into a problem and run in circles trying to debug weird underlying language design decisions.
- Gibbon1 5y agoMakes me wonder if using keywords like var char foo; // foo is a char pointer char boo; // pointer to a boo type or something I want. alias baz = boo.foo; // bas is actually just an alias for boo.foo
- andi999 5y agoHaskell does not really have pointers, but let's say it does (or that ioref fulfills it). How would you then (as an example) declare an array of pointers pointing to functions accepting int and returning float declare in Haskell?
- Ar-Curunir 5y agoLike How Rust does it? via Box or using &
- lifthrasiir 5y agoIt would be `[fn(i32) -> f32; N]` for the reference (equivalent to `float (*[N])(int)` in C). Box or & would be only required for also allowing function-like values, as in `[Box<dyn Fn(i32) -> f32>; N]`.
- anyfoo 5y agoArray Int (IORef (Int -> Float)) The first "Int" is the type of the array's index, which in C is always "int" and hence not specified. (As you note yourself, pointers, or even just arrays, are not very much of a Haskell concept, but I still think this is pretty readable and very obviously parseable if you know the involved types "Array i e" and "IORef a".)
- FeepingCreature 5y agoFor a C-like take, D: float function(int)[], or if you genuinely wanted an array of pointers to function pointers, float function(int)*[].
- andi999 5y agoThanks. Yes I wanted array of pointers. So then how would you do a pointer to an array of functions in D? * float function(int)[] ? Edit: or float function(int)[] *
- FeepingCreature 5y ago
- pansa2 5y ago> Students have enough trouble mentally separating the concepts of declaration and usage I wonder if this is an argument in favour of Python’s implicit declarations (“there are no declarations, only usages”) or against them (“there are still declarations, but they’re invisible”)?
- anyfoo 5y agopython is dynamically typed though, so most explicit declarations would be boring and pointless (hence the easy omission). More to the point is probably type inference in statically typed languages, e.g. in Haskell you can often omit the type, or in C++ you can just call it "auto". I'd say to learn a language it's better to explicitly specify your types in order to understand them, but I'm not sure it's necessarily "confusing" to omit them. I.e., "it has a type but I don't know it" is much easier to deal with than "what in the world is that type supposed to be".
- pansa2 5y ago> most explicit declarations would be boring and pointless Not pointless - in similar languages (e.g. JavaScript's `let` and Lua's `local`), although declarations don't indicate a variable's type, they do indicate its scope. Without explicit declarations, Python needs to make an assumption about scope - specifically, that each variable's scope is the innermost containing function. This makes the language easier for beginners, but more complex overall because it requires special features to support closures.
- codeflo 5y agoIt's also a lot more error prone in large programs because misspellings introduce new variables instead of causing an error.
- macintux 5y agoThe fact that you can “declare” variables inside logic branches also leads to runtime exceptions when the code flow doesn’t touch any of those branches, but still expects the variable to exist later.
- Rerarom 5y agoI was so confused as a student because I has roughly the same objections as you have here, but I thought it was I who was missing something. Also: In an hour I'm teaching pointers to novices haha
- adrian_b 5y agoThe problems caused by "*" in C/C++ declarations and also in expressions are caused mostly by the mistake that "*" is a prefix operator instead of being a postfix operator. This mistake has already been recognized by Dennis Ritchie a very long time ago, but it could not be corrected as it would break backward compatibility. In 1965, the language EULER by Niklaus Wirth & H. Weber was the first high-level language with explicit pointer variables (previous languages, e.g. LISP I, FORTRAN IV and CPL used pointers only implicitly) and with the 2 operators "address-of" and indirect addressing, the ancestors of C "&" and "*". However in EULER the indirection operator was postfix, as it should be. Niklaus Wirth kept this operator as postfix in his later languages, e.g. in Pascal. With a postfix indirection operator, when you write some element of a hierarchical organization of data, which can include an arbitrary number of the 3 operations, array indexing, structure/class member selection and pointer indirection, all the operations are written and executed from the left to the right, without any parentheses and without any ambiguities. (also the C operator taken from PL/I, "->", is no longer needed, as it has no advantage over writing "*."). This simple change would have avoided most pitfalls of the C declarations.
- DeathArrow 5y agoI don't think prefix usage of * operator is a big flaw. I've learned C/C++ in high school and never had a an issue with it. I've understood that the operator is applied to the variable, not to the type and that declaring a pointer is not the same thing as dereferencing it . Even if someone may have made a mistake when declaring a pointer in C or C++ it is something they can get over pretty fast.
- habibur 5y agoI didn't understand it either. Than learned assembly for a few months for a project -- switched back to C and everything came as natural.
- ncmncm 5y agoDereference syntax in C++ is very, very fixable. C++ could easily adopt a postfix operator^ as a synonym for prefix op*, even at this late date. Then (*p)->m could be written p^->m, or p^^.m. (The last is clearly best.) Rust, too, could do the same, for the same benefit. And C could, too, if there were any point. The only people to complain would be IDE vendors. If you were then allowed to declare a pointer argument like "char p^", or even "char^ p", you could begin to dispense with use of "*" as a unary operator. Declaring "char^ p, q" would mean both p and q are pointers, eliminating another source of beginners' confusion.
- veltas 5y agoI think this was only considered acceptable because in early C you couldn't initialise automatic variables. So you would never be confused by an association like `char *p = q; *p = q;`, because you couldn't write that first part. And declarations were all separate at the front of the function.
- agent327 5y agoWhile you are not wrong, the two major problems in C I would consider to be array-to-pointer decay (arrays should really have been a separate type), and assignments being expressions instead of statements. Array decay means that pointers need the ability to point to different array elements, thus requiring pointer arithmatic - something that wouldn't have been needed at all without array to pointer decay. And since assignments are expressions, they must return something, meaning they can be used in statements like 'if (a=b)...'. If they were statements they wouldn't return anything, so this mistake would be impossible. You'd lose assignment chaining, but the win here would be much greater than the loss. If I had a third choice, it would be the mess around 'char'. At the very least there should have been two types: one for characters (where signedness is irrelevant), and one for the smallest type of integers (that can exist in signed or unsigned form). And in these modern times an additional type, something like 'byte', would be needed to deal with aliasing issues, so char (and things like uint8_t) can exist without that, allowing better optimisation for those types.
- eMSF 5y agoArrays are a separate type.
- SkeuomorphicBee 5y agoJust in the exact scope in which they were declared, once they are passed as argument to any function they become a simple pointer.
- agent327 5y agoNot from pointers, though.
- nsajko 5y agoI also dislike array-to-pointer decay, but I'm not sure why do you say that it is one of the two major problems. Could you expand on "Array decay means that pointers need the ability to point to different array elements, thus requiring pointer arithmatic"? I don't see what you mean. A point that comes to mind is that if array-to-pointer decay were really such a bad thing, the "struct of array" and "pointer to array" types would be used much more than they are (they are almost never used).