For those less familiar with C I'd like to point out that a common solution to the "function pointer hell" in C is to alias the types with typedef, although one might argue it's more a workaround than a real solution.
So the last example of function pointer taking a function pointer parameter and returning a function pointer could look something like:
typedef int (*return_fp_t)(int, int);
typedef int (*param_fp_t) (int, int);
return_fp_t do_stuff(param_fp_t a, int b) {
/* ... */
}
It requires coming up with new type names but it remains pretty readable in my opinion. Also if the prototype of one of the function changes (which is pretty common in my experience) it's easier to modify it since it's only declared once.
Not mentioned in the article is the typedef problem in C.
Early C didn't have typedef, just struct. The grammar was LALR(1), and context-independent - knowing the syntax and the reserved words, you could parse C.
Then came typedef. Now, to parse C/C++, you must know the names of types. So, to parse a C/C++ file, you must read and interpret all the #include files first. This makes it hard to write tools that work on C/C++ source.
This isn't a problem with the declaration syntax of the Pascal/Modula/Ada/Go line of languages, where declarations have the order "x int" instead of "int x". So "go fmt" doesn't have to have all the dependencies just to clean up the indentation.
Go has a similar problem: function calls and type conversions share the same syntax (or sufficiently the same syntax). If you see `x(t)` in a Go file, there's no way to know if that's a function call or a type conversion without some semantic information. Also built-ins, which also look like functions, come in to add an extra layer of difficulty.
I think C declaration syntax is a bit of an acquired taste --- it looks confusing at first but once you "get" it everything falls into place and it feels completely natural. Arguments that it is difficult to read because it is not "left to right" contrast with the fact that the expression syntax is not "left to right" either in most programming languages; 2 + 3 * 4 parses as 2 + (4 * 3) in most languages that use this syntax.
You know. I was working on a project last day and a horrible bug happened where the timing system broke and spammed my console and corrupted my database.
In C, of the 3 type operators you compose to declare the type (arrays, functions, pointers), 2 are postfix (arrays and functions) and one is prefix (pointers). This causes a mixture of directions when you read the type. Here in go they fixed this by making arrays and functions prefix like pointers, so you get a unidirectional read and you don't need parenthesis for precedence (except maybe for associativity).
The problem is that functions, in C, are neither prefix nor postfix. A postfix function would look like:
int (int argc, char*[] argv)main
prefix is:
main(argc int, argv []*char) int
The C version:
int main(int argc, char *argv[])
Is a horrible jumbling of both styles, with some confusion around type and name thrown in for fun.
The main benefit of the Go approach, beyond whether prefix or postfix works better, is consistency. Compare to Go's:
main(argc int, argv []*char) int
Once you learn to read it, it's much better, though perhaps not like English. In English, we preface the noun with the adjective, "the red car," whereas other languages say the equivalent of, "the car red." When people object to the Go syntax, they're almost always native English speakers.
Ptr(Type) -- deref type
Array(Count, Type) -- elem type
Func(ParamList, Type) -- return type
Since the "Type" param is always there. C declaration syntax is like a linear composition of operators where the Type flows through all the operators to form the final type. The base type on the left of the declaration is the initial parameter that goes through the composition.
So C type operator grammars (in the right-hand side of the declaration, excluding the base type) are actually:
That is why I say ptrs are prefix and array/func are postfix.
All 3 have the extra implicit type argument that comes from the wrapping type-expression and the base type - so I wouldn't include it and say they're both prefix and postfix.
After spending some time in a language with left-to-right declarations, going back to spiral declarations makes you realise just how many contortions your brain has to constantly go through. left-to-right is a breath of fresh air.
Edit: Of course I must add that it is a breath of rather old fresh air - Pascal and SML have had it for decades.
Many times I want to program in C or try to debug some open source apps but I'm always reminded why I didn't touch C/C++ since high school: macros, #include, CONSTANTS, long_identifiers, ugly castings everywhere and cryptic function pointers with the dreaded clockwise spiral rule. I still wonder how the world would look like today if instead of C++ we would have had a C v2 binary compatible with C but with type safety, interfaces and syntax similar to Go, or even this c2lang.org.
C is great, but it is very limited. Mainly, it knows almost nothing about data structures (strings and arrays, with the absolute minimum support)
Merely reshuffling the syntax/grammar but not going deeper is not going to change anything.
C++ is, let's say, interesting. But it's always going to be a "bolt-on" addition to C, and keeping the redundancy (should I use open, fopen or a C++ object? when opening devices, open is usually used)
Yeah, having gone through latest version of Effective C++, updated for C++14 I have not really sure if I want to step away from JVM/.NET worlds back into C++ that I left in 2005.
Working alone in C++ is one thing, but having to read other people's code seems to turn into a mess if you have a mix of C++98, C++11 and C++14 code bases.
I don't mind the declarator syntax in C so much. What I do mind is the way some things are implicitly converted. For example: in
char buffer[10];
the name "buffer" is a pointer to the first element of the buffer. This always feels like a hack, because "buffer" is an array, not a pointer to an element. I'd rather write &buffer[0]. This also feels like a hack when the buffer is coincidentally of size 0, and buffer[0] does not exist. So I'm just left with the feeling that something is missing in the language.
The short version is that in the early days, pointers where declared like "char p[]" and for arrays declared like "char buf[10]" a pointer was created, like this (in modern C):
char buf_storage[10]; /* this is not actually visible to the programmer */
char *buf = &buf_storage[0];
So pointers and arrays really were the same thing. Of course, this didn't fly anymore when structs were introduced (or when embedding arrays in structs was allowed, at least) so that's when arrays were introduced as a first-class type, with weird semantics so that existing code that used array variables as if they were pointer variables wouldn't break.
You're right about the implicit conversions but "buffer" is not a pointer to char. The type of buffer is "array of 10 chars" and sizeof(buffer) equals 10, not sizeof(char*) which usually will be 4 or 8. Personally, being a lazy typist, the implicit conversions from array to pointer-to-first-element have never bothered me as it allows you to write terse code and the cases where stuff gets complicated are rare and can be clarified by typedefs, as somebody already pointed out.
I don't code in C very often, but when I do, I'm always annoyed that when I see a declaration `type ∗x`, I don't know whether `x` is meant to be a "simple pointer" or a an array. At least with Go, you clearly declare `type []x` or `type ∗x`.
P.S.: how do you input a literal asterisk on HN? Escaping with a backslash doesn't work, had to find a fancy unicode asterisk.
Yeah, or that the declaration syntax of pointers are "intended to be a mnemonic". It's syntactic sugar that, instead of being a convenience, simply misleads you. C is a language where you need to know what's going on and because of these weird decisions knowing how things work actively hinders you when writing code.
int p;
type nameOfInstanceOfType;
int *p;
notType notNameOfInstanceOfType;
How about something sane like: "pointer p = pointer(int)" or even Scalalike "pointer[int] p"? Pointers aren't so bad really. I think a lot of the hate for them is because the syntax begs you to fuck up. And inevitably you do. I mean, I just type random stuff until I get no warnings and things look like they work. Literally. That's how I program. How many asterisks here? I dunno, I'll just try zero through three and see if any of those work...
Well, it would be fairly inconvenient to have to type strcmp(&some_name[0], &some_other_name[0]) all the time. Or having to type const char ∗s = &"some constant_string"[0] -- the type of a string literal is after all "char[size]".
As to your other point: it's legal for a pointer to an object to point one item past the end of the object. You may not read/write to this of course, but if you allocate 100 bytes of memory you may have char ∗start, and char ∗end = start+100. That's a nice concept taken to its limit in STL where all algorithms operate on abstract start; end+1 iterator pairs. So an empty sequence has start == end.
Well, you can declare your variable as const char s[] = "...". And if strncmp were to be defined today, it really should have a prototype of int strncmp(size_t n, const char s1[static n], const char s2[static n]). This is valid C99, although it seems nobody writes code like this and prefers to degrade their arrays to pointers. (Today's C also has pointers to arrays, so the need for dynamic allocation is no valid excuse either.)
This hits home, I have spent many evenings designing a language that is semantically equivalent to C++ (in fact, it's made to transpile to C++11) but with a LALR grammar (using just flex and bison). Glad to read that none other than Rob Pike agrees with many design choices in my language :)
I've yet to see any evidence that Go is all that fast to compile, but whether it is or not there's simply no way that the syntax will be what makes a difference vs C.
Compilers like GCC are "slow" because they do a lot of work. To see a good demonstration of how fast C compilation can be, consider Bellards TCCBOOT [1] which boots into a version of TCC that then compiles a Linux kernel and executes it - the compilation took a reported 10 seconds on a 2.4GHz Pentium 4 in 2004. When testing against the then-current gcc 3.2 with -O0, TinyCC was about 9 times faster than GCC.
There are certainly things about C that makes fast compilation tricky, but it's not the syntax (abusing a text based pre-processor instead of having a proper module system being suspect #1)
Even so, last time I looked, GCC stood up fairly well agains the Go.
So the last example of function pointer taking a function pointer parameter and returning a function pointer could look something like:
It requires coming up with new type names but it remains pretty readable in my opinion. Also if the prototype of one of the function changes (which is pretty common in my experience) it's easier to modify it since it's only declared once.