Edit: And the intrusive data structures commonly used in C are way more complicated than the equivalent generic code (in my opinion).
A red-black tree implemented in the same simple style by an OpenBSD developer a couple of decades later was adopted almost overnight by all BSDs: https://man.openbsd.org/tree#EXAMPLES
Unlike the Linux version, the BSD version is as type-safe as a C++ template, permits a node to exist in as many collections as you want (one member field per collection), and like a template the BSD version can inline the comparison function. You even have the option of making all the routines extern or static, which you can't even do with C++ templates.
What the Linux version has going for it is that it doesn't need to rely on the preprocessor so the implementation looks a little neater and more like a C++ template--no columns of backslashes as required with preprocessor-based generation. But that's a horrible tradeoff. queue.h and tree.h have basically been frozen for decades.[1] After initial curiosity wanes (day #2 of many thousands), nobody reads the headers because the interface is simple and never changes.
[1] Various systems have tweaked things here and there (e.g. to add parameterized trace points), but the original APIs never changed and code has always remained portable at the source level. The only big change over all that time was most systems (BSDs, macOS) eventually dropped CIRCLEQ after someone (I think a FreeBSD developer) posted benchmarks showing it sucked relative to TAILQ. I actually preferred CIRCLEQ; the entire header is small enough that for awhile I just copied it around everywhere.
- Download one that uses interface{} / void pointers - okay, but not very efficient or type-safe.
- Download one that's intrusive - okay, but pretty convoluted.
- Write my own - a pain.
- Code generation - generics but worse.
(This isn't sarcastic.)
But if you are working on a project where the details of the tree do not matter (so you can use a library), you will probably use C++, not C. I still have a hard time understanding what niche Go fills, it's too high level to compete with C or Rust, but here we are comparing its expressiveness with C.
The overhead of interface{} isn't as high as people sometimes think. You have the overhead of boxing the value, but the actual type checking is just a single comparison and jump.
This is way too much in a tight loop. I want my structure to fit in the cache and I want to have as little tests as possible.
If that is a hard requirement, then the answer is just that Go isn't a suitable language. AIUI the current generics proposal in Go, it won't fix that problem for you either.
Where I disagree with the programming community at large is how often that's actually a hard requirement. A lot of programmers grossly overestimate their performance requirements on the microscale [1], and so concerns about "premature optimization" remain alive and well decades later. However, there are undeniably still a non-trivial number of cases where that really is a hard requirement, in which case the answer is, don't use Go.
(Preferably, figure that out before you start. As a fairly heavy Go user, I actually kinda cringe when reading one of the several "We sped up Go by 10x on our particular big thing and got to barely-acceptable performance" articles, rather than consider it something to be celebrated. I mean, cool that you sped it up that much and all, but honestly the correct answer was to notice in the design phase that you were doing something you probably shouldn't have done in Go, modulo concerns about easy prototyping and such.)
[1]: I qualify with "microscale" because I also think a lot of programmers underestimate the performance requirements on the macro scale. But that's actually a very different issue. There's a lot of very sluggish and slow code out there in the wild that is architecturally wrong; at every individual step the code is reasonably optimized and fast, the problem is having the wrong steps.
But yes, if you're writing performance critical code (which you generally won't be), then you'll need to use code generation to avoid boxing.
This kind of discussion is pointless without some kind of real world reference point. The vast majority of Go projects will never run into a situation where the overhead of using interface{} is a performance bottleneck. If you're working on a project where you consistently have to think about the cache performance of your code, then C++ or Rust would probably be better choices. Go isn't meant to be all things to all people.
I have different kinds of constraints, mainly propositional clauses (a or b or c) and pseudo-boolean constraints (2a + b + c >= 2). Long story short, a propositional clause is a special kind of pseudo-boolean constraint, so I can process my clauses as if they were regular pseudo-boolean constraints, but this is slower than using dedicated code.
Lots of problems only deal with propositional clauses, so they take longer to solve than they should, because I cannot say "this is a problem with only propositional clauses", vs "this is a problem with all kinds of clauses". I could use interfaces, and tried, but this is way slower, to the point it doesn't make sense to use them.
And I can't use C++ or rust for this problem: there are already tons of very good c++ SAT solvers, but my goal was to provide a SAT solver I can use in my go programs without resorting to cgo black magic.
Yes, fair enough, I think that is a good example of the kind of code that Go is not particularly suited to (if performance is critical).
The cognitive overhead certainly is. Computers exist to help us do things faster and with fewer mistakes. This is a prime example of using a computer to help us make more mistakes, worse.
There is no doubt that not having generics makes some code worse than it would be with generics. But I think the problem is often overstated.
I honestly wonder if go isn’t cursed to follow the same road.
Blaming just generics for that does not work imho; in Java people were writing really insanely over complex code close after the first release; I had an ex C++ guy before 2000 writing Java code that used every design pattern in The Book, everywhere he could and it ended up with 1000s of files and those strange long names that became the standard for Java a few years later.
But yeah, I share your concern. It would be nice to have a better way to write container types in Go, but generics always seem to end up getting abused.
You can build any abstraction you want in C.
This is disingenuous. Show me Pi types in C.
I took a quick look at what these are. Couldn't they implemented with macros like (this builds a type "Utf8String" which "depends" on the value of the "ENCODING" parameter to figure out what set of functions to call for the guts of the functions that handle the type, e.g. RawStringUtf8Length):
enum Encoding { Ascii, Utf8, Utf16 };
#define TYPENAME Utf8String
#define ENCODING Utf8
#define CHARTYPE char
#include "stringdef.ph"
Then in stringdef.ph (ph=parametric header, just came up with it): typedef struct { size_t len; CHARTYPE* text; } TYPENAME;
#define MAKEFUNC(name) TYPENAME##name
#define RAWSTRINGFUNC(name) RawString##ENCODING
void MAKEFUNC(Create)(TYPENAME* str);
void MAKEFUNC(Destroy)(TYPENAME* str);
void MAKEFUNC(CreateFrom)(TYPENAME* str, const CHARTYPE* text);
/* this goes in a single C file, single header library style */
#ifdef IMPLEMENTATION
void MAKEFUNC(Create)(TYPENAME* str) { ... }
void MAKEFUNC(Destroy)(TYPENAME* str) { ... }
void MAKEFUNC(CreateFrom)(TYPENAME* str, const CHARTYPE* text) {
str->len = RAWSTRINGFUNC(Length)(text);
str->text = RAWSTRINGFUNC(Duplicate)(text);
}
#endif
#undef MAKEFUNC
/* cleanup input parameters so includers wont need to do it */
#undef CHARTYPE
#undef ENCODING
#undef TYPENAME
Then you can use the above simply like Utf8String u8s;
Utf8StringCreateFrom(&u8s, "foo...");
It isn't exactly pretty, but seems to do the work. The ENCODING parameter is only conceptually an Encoding enum, but this can be enforced with some dummy (unused) code that does use of the ENCODING value as Encoding (and if extra strength is needed, as enums are weakly typed, instead of native enums, you could define macros that create stronger "enums" out of structs).Interpreter, but close enough :)
Embedding a higher level language that's controlled from a more primitive host language allows solving many otherwise tricky problems without the struggle.