This was the biggest mistake IMO. Go’s declaration syntax needs colons, it would have been 100x more readable.
Compare:
func foo(x int, y int) int {
var sum int = x + y
return sum
}
To: func foo(x: int, y: int): int {
var sum: int = x + y
return sum
}
The colons would have made it so much more clear to read for me.It's especially terrible in C and C++, where you need to be on your guard for whether all the variables are being declared with the same type or whether some are pointers. Even in languages without this quirk though, I find it still slows my comprehension.
Unlike readability, conciseness of code isn't a virtue in and of itself. I see little upside to multiple-declarations, but clear downside.
Language trivia: the D language does not follow C's rule, instead it guarantees all variables in a multiple-declaration statement all have identical type:
int* x, y; // x and y are both pointers to ints
https://dlang.org/spec/declaration.html#declaring-multiple-s... var x, y *int // both pointers to intSo instead of this:
func foo(x: int, y: int): int {
var sum: int = x + y
return sum
}
You’d have this instead: func foo(x :int, y :int) :int {
var sum :int = x + y
return sum
}That makes as much sense as `int *i`, when `int* i` is obviously the correct (though heterodox) way.
int* i, j;
C declarations simply are not "first the type, then the variable"...if you dont have space for that one line, switch back to punchcards
In my code I never use it. Every variable gets its own line. You might think this would blow up the declarations, but in practice it's not a problem, especially in modern C where you can move declarations down to where they're used instead of stuffing them all up at the top of the function.
People think that "int *x;" means "declare x as a pointer-to-int" but really it means "declare x such that *x (it's a code expression) is of type int." So it's a really roundabout way to say that x is a "pointer to x" while that concept of "pointer to" isn't even in the language syntax.
It's why I always cringe when I see
int* x;
as opposed to int *x;
There is a reason why K&R do it that way.I get that this is a pretty fundamental disagreement between me and Knuth & Richie, but I think history has shown that this idea of splitting exactly one aspect of the type (if the variable is direct or a pointer) was a mistake. It's been the cause of a lot of bugs and the cases where you want to define both direct and indirect types on the same line basically don't exist. For clarity you should split those definitions up regardless of what the syntax allows.
There are basically no cases where you should be writing the following code even if it is technically correct:
int *pointer, direct;As to your other concerns, it makes perfect sense.
int WHATEVER;
read as "WHATEVER is an int" and you can work backwards from there. There is no more type syntax than that. The thing is, it's much easier to look at stuff this way since otherwise you'll have to shift back and forth between 2 different syntaxes (types and expressions) all the time.I've been looking a lot for a better more algebraic syntax too. All syntaxes I've found have lost on two axes _in the common and simple cases_
- low cognitive overhead
- terseness
Yes, the principle of declaration follows usage is already stretched to its limit, and C++ has certainly overstretched it. But still for the bread-and-butter use cases, the "better" syntaxes seem to always lose on those 2 axes.
Also, a code such as:
int*x=y;
will define the initial value of x and not of *x. For this reason, a space before the asterisk can be confusing in this way. And, for the reasons described in another comment, putting the space after the asterisk is also confusing. So, I find it clearer to omit both spaces.When I require more complicated types, I will generally use typedef (or typeof) instead of having to deal with the confusing syntax of types in C.
The rest of your post is perfect and objectively (subjectively) correct.
While I fortunately knew that `int* x, y` means `y` is not a pointer, I always assumed it was some kind of capricious oversight of the compiler. I had no idea the rationale was "the expression `*x` declares `x` to be a pointer to the type west of it". That strikes me as so weird! XD
This was and remains a bad decision, full stop.
'Pointer to something' should have been a parametrised type rather than C's weird syntax of overloading operator*. And I won't accept the argument that C was too old to have parametrised types: ML, Lisp, and many other functional-first languages developed alongside C and had them. We've just stuck with C because it got popular because of UNIX and then Linux.
I avoid raw pointers whenever I can when I write C++, and I yearn for a language that exposes pointers with a rich and expressive type system instead of making them glorified 64-bit unsigned ints. Pointers ought to be one-dimensional unsigned parametrised affine space types[1] (note: not affine types, which are completely different[2]), with several implications:
- comparing pointers to different types should be invalid
- subtracting pointers to the same type should return a signed 'pointer difference' type
- adding pointers to the same type should be invalid
- Get the address contained by a pointer with Ptr<T>::address()
[1]: http://videocortex.io/2018/Affine-Space-Types/
[2]: https://en.wikipedia.org/wiki/Substructural_type_system#Affi...
Programming languages are a huge waste of time. It's a domain with lots of opportunities for bike-shedding, and then for hard-core R&D too. But programming is still engineering, simple & easy that gets the job done wins over needlessly complex. And yes, inertia is real too, but if a programming language was dramatically better at what C does best, I wouldn't still start new code in it.
When you're doing systems programming, pointers being "glorified" 64-bit ints is pretty much what you need. When your requirements hit reality, all the nice abstractions break down quite easily.
> I avoid raw pointers whenever I can when I write C++
Empirically, this is the path to madness.
> - comparing pointers to different types should be invalid
It produces a warning on my C compiler and an error on C++ compiler. Note that there _can_ be pointers containing equal addresses with different types -- a struct and its first member have the same address.
> - adding pointers to the same type should be invalid
I think it is, or what exactly do you mean?
> - subtracting pointers to the same type should return a signed 'pointer difference' type
it does, that type is called ptrdiff_t, which is a signed integer type.
> - Get the address contained by a pointer with Ptr<T>::address()
But you didn't want them to be glorified 64-bit ints? Well, because of pointer arithmetic, which is a practical necessity, they kind of are, but then again they're not because C tries to abstract from that. You can kinda unwrap the pointer abstraction by casting to the (optional) uintptr_t type.
The thing is, not all machines necessarily have one flat address space that contains everything from functions to all data. Pointer representation isn't necessarily uniform -- e.g., you can have a bigger type for void-pointers that contain type information too, this way you could do run-time type checking.
I read it like 'IntegerPointer x' or 'x is an integerPointer' or 'x is a pointer to an integer'
It rhymes with the following : 'MilkGlass m' reads as:
'm is a MilkGlass' or 'm is a Glass of Milk'
int* x[200];
You can fight all you want, but C doesn't have type syntax. What you're looking at is expression syntax.Now do a pointer to an array of 200 ints.
'a is of type one hundred intPointers' should, to be more consistent, be written as (int*)[100] a;
I'm not even sure whether that works, never tried it.
The fact that arrays are automatically pointers is very confusing for beginners, especially multi dimension arrays are brainbenders in that respect.
int * <---> foo;
int [] <---> foo; // ugh no!
The latter would work if C had been designed differently, with operations defined on types. But it was designed with operations defined on values, not on types. There's not a way to express "foo is a pointer to int", you must say "when you dereference foo you get an int" (in fact, * means dereference, not pointer).Imagine if English had not a word for "son". To say Bob is Adam's son you would say "Bob's father is Adam".
To tell the truth, C does have the dual of * that is &. If operations on types were defined (and they are not!), then we could have a syntax like this:
&int foo;
In any case, we wouldn't solve the issue with arrays, because, while [] is the operator that, given an array, returns one element of it, its inverse operator doesn't exist.It's not difficult to imagine a different language with a rich set of operators defined for types. We could have &T for "pointer to type T", Tn (note the infix , as multiplication) for "array of n elements of type T", and so on. Then declarations could be easier:
&int ptr;
float*3 rgb;
&int*8 foo; // array of 8 ptr
&(int*8) bar; // ptr to array
See how we are using full expressions including parentheses with types?At this point, you would also move the variable to the left and the type to the right, and maybe add a colon for clarity:
ptr : ∫
rgb : float * 3;
foo : &int * 8;
bar : &(int * 8);
But this is another language, not C. >foo : &int * 8;
>bar : &(int * 8);
while just about anything is better than the original, we could certainly do better. I am curious how you expected to handle function pointer syntax, as that is the usual bear of C typing.let's aim to make the types unambiguous, simple to read and write
all C types can take at most one parameterizing type, so prefixing works unambiguously.
force const and volatile to be exclusively prefixing as well, sorting the weirdness trailing consts would allow.
() can represent a function-pointer, with its return type following
your examples would be
ptr : *int
rgb : [3]float
foo : [8]*int
bar : *[8]int
that would leave complex : a [](int, int)**(int,int)*const*volatile*(int,int)[][]*[][16]*char ;
vs char *(*(* volatile * const (**a[](int , int ))(int , int ))(int , int )[][])[][16]
I couldn't have written that second one without https://cdecl.org/Of course, this simple prefixing is also exactly what Go did to improve the types, with some syntactic sugar to help the reader along.
However, I strongly believe well-known symbols should keep their properties independently of whether they are used in type expression or in value expressions. So, for example, [] is already a postfix operator: it would be confusing if it was sometimes a postfix sometimes a prefix. And the unary * is already used to dereference, it would be weird if sometimes it was used to take a reference instead (for that there's already the unary &).
And don't forget the precedence: you shouldn't redefine it. So &int[] would mean &(int[]) not (&int)[].
And since we're talking of a hypothetical new language, for function types I'd choose to re-use an existing operator although for a completely different meaning: ->
Note that structs and typedefs that you define are proper type names, you can switch out "int" for them. You can easily "typedef int *intPtr;" and then merrily go "intPtr myptr".
(I think cast type-specs might be a bit of an exception here were the rule breaks a bit -- because they don't "use" a name in the cast "expression". I don't intimately understand how those work. The whole thing is an idea that was stretched to the limits, especially with C++ where consistency has pretty much been given up).
:does this look correct to you?
I can only assume he removed them, not for brevity, but as a compromise for the sake of C programmers to only ask them to learn one new thing rather than two.
func foo(x: int, y: int): int {
sum : int = x + y
return x + y
}
With type inference the type declaration just collapses out to what you have now. func foo(x: int, y: int): int {
sum := x + y
return x + y
}
I've seen it in a few languages and it has seemed quite nice in use and appearance to me. return sum int func foo(int x, int y) {
var sum = x + y
return sum
} foo :: proc(x : int, y : int) -> int {
sum : int = x + y
return sum
}
or foo :: proc(x : int, y : int) -> int {
sum := x + y //
return sum
}