Zig looks really cool but it feels like it has a high chance of being a niche language. Rust never felt like that.
Zig looks really cool but it feels like it has a high chance of being a niche language. Rust never felt like that.
C++ has approximately eighteen different partially-overlapping categories of variable initialization, many of which are legacy-but-still-used! [0] And some of those categories have changed their boundaries every language version since C++11 (based on the definition of "aggregate").
C++ has three to five partially-overlapping kinds of type inference all in active use (auto, decltype(id), decltype((expr)), decltype(auto), template argument deduction)! This is interleaved with name lookup and overload resolution (below), so if some subexpression isn't compiling how you expect, you have a vast space of language features potentially to blame.
C++ has so many kinds of name lookup and namespacing that I'm not even sure how to count them. There's unqualified lookup, argument-dependent lookup, qualified lookup, class member access, etc. Sometimes you can't refer to things defined later (outside a class) and sometimes you can (inside a class). There is even undefined behavior if you mess up namespacing! (Undiagnosed ODR violations are every experienced C++ programmer's nightmare.)
C++ has ad-hoc overload resolution based on un-scoped identifiers, which even crosses namespace boundaries using one of the above name lookup modes. It has two kinds of user-defined implicit conversions ("explicit" and implicit) that also affect this selection process, on top of the zoo of "type promotions" inherited from C.
C++ classes have five to six kinds of "special member functions," some of which may be defined automatically by the compiler, each with its own rules for when and how, based on what else is defined in the class. These also contribute to overload resolution, of course.
[0]: https://blog.tartanllama.xyz/initialization-is-bonkers/
Rust has a complexity of its own, but it's quite different in scale and quality. There is exactly one way to initialize a variable, exactly one kind of type inference, only two ways for names to be resolved (directly or via an imported trait). Overloading, implicit conversions (of which there is only one kind, Deref), and the replacement for "special member functions" (Copy, Clone, Drop) are all based on exactly one mechanism (again, traits).
The article has a pretty accurate description of how people experience Rust's remaining C++-like complexity, IMO: "I don't remember the order in which methods are resolved during autoderefencing, or how module visibility works, or how the type system determines if one impl might overlap another or be an orphan."
But one important aspect it leaves out (not being a C++ article) is that if you mess up any of these, you just get a compiler error- and Rust is well-known for having extremely helpful error messages. In C++ you may get a compiler error (known for being extremely unhelpful) or you may get undefined behavior.
Zig is certainly a smaller language than either C++ or Rust, but that comes at a cost. I would much rather hear discussion of those actual trade-offs than yet another "Rust is just as complicated as C++" non-claim.
It's definitely a matter of personal taste, but to me, Rust seems a monumental, awe-inspiring shrine erected to worship at the altar of accidental complexity. I admire the technical achievement -- I never imagined accidental complexity could be given such spectacular prominence -- but having spent my share of time with both C++ and Ada, I'd like to look elsewhere. I don't know if Zig will do the job, but the vision it has for low-level development is so refreshing, radical and different from everything else I've seen in my >20 years of professional software development that I'd like to give it a chance before settling for an improved C++. Anyway, it's a matter of personal aesthetic preference.
I'm also not trying to convince you to drop Zig and settle for Rust! You can use either or both or neither, I don't mind! Rather, my point is that Rust's direction relative to C++ is the same one you praise Zig for- it provides the same control with drastically more economical application of fewer language features. Just because Zig goes further (again, at great cost to things like tooling, error checking, and messages) doesn't mean Rust didn't make a lot of progress.
BTW, I haven't "adopted" Zig that I would need to drop it for anything (it's not even 1.0 yet). I'm still with C++ for the time being. But I'm deeply impressed by Zig's revolutionary design and complete rethinking of low-level programming that I'm keeping a watchful and hopeful eye on it.
But this makes me suspect we may be using very different definitions of "accidental complexity" here: Your usage seems to apply to programs, which wind up over-specifying low-level details in both languages. My usage of the term applies instead to the languages themselves, and the level of extra pain they inflict on programmers who have already accepted the C++/Rust/etc aesthetic.
C++ is highly backwards compatible to C to the degree that it's almost (but not quite) a superset. Of course it's quite easy to start creating cpp files. Also C++'s benefits were quickly realizable while for Rust's safety benefits to play out, you need to have replaced significant portions of highly risky components (e.g. those that parse user data, have a history of bugs, etc). Adding Rust to an existing C++ codebase is much harder than adding C++ to an existing C codebase.
Also do you have a link for your claim? I'm interested in reading on the early rise of C++.
so, uh, how does Rust define behaviour if you export a same-name symbol from two rust dlls and load it from some executable ?
The only way to get matching names is to ask for them explicitly, via FFI.
but, after checking apparently this adds a hash of the function to the name mangling - how does that work when you want to call it from another language ? e.g. for instance you can call C++ code directly through some dialects of Lisp, Perl , ADA, or D (AFAIR) as they all have libs or mechanisms that kinda understand C++ name mangling - how are you going to do the same with, from what I'm seeing, "name_of_the_file::name_of_the_function::some_hash" ?
> The only way to get matching names is to ask for them explicitly, via FFI.
and what happens if you have two libraries which expose the same extern-C function name ?
If you do wind up exporting the same name twice, you just get a linker error, because Rust doesn't play the same games C++ does with linkage. (This is also true of C++ FFI- the problematic ODR-violation stuff tends to involve more complex language features than `extern "C"`.)
this does not answer the question of whether the behaviour is defined if multiple libraries export the same name (which is the original question). See my other comment, what happens if from rust code you dlopen libbar.so ?
that's the same for every language and thus not very relevant. if you have single, static binaries / libraries of course everything is simple, and you'll get linker errors in C++ just like you would in Rust. What is not simple is when you start loading twelve dozen libs at load-time or run-time and it does not seem that Rust defines behaviour any more than C++ in that case.
You seem to be under the impression that dlopen somehow interacts with language undefined behavior; it does not in either Rust or C++.
but it is ! the only reason why ODR is UB in C++ is because the C++ language authors can't force the system linkers (again, whether at link time, load time or runtime, I'm not only referring to dlopen) to perform LTO which trivially makes ODR violations a diagnosticable error.
But as far as I know, neither can the Rust language authors do so - so either the behaviour in Rust is as defined as in C++, or Rust does not support creating standard platform object files that are linked by ld, gold, or whatever (such as D, ADA, Fortran etc all support) which would make a fair amount of use cases impossible - it's pretty common in some HPC circles to link C++ and Fortran directly in the same executable for instance. And then, of course it's easier to define behaviour when you use a reduced set of constraints, but it definitely does not makes something worth bragging about.
Normal, non-FFI-using C++ can hit ODR violations in response to things like typos or subtle mis-uses of `inline` and templates.
Normal, non-FFI-using Rust is designed such that these situations never come up.
My original comment was never talking about FFI in the first place, where yes, both languages are much more at the mercy of what the platform provides. However, in that case the spooky UB ODR violations I was referring to are also not relevant, because you just get normal, fully-defined platform behavior- the expectations of the compiler (and thus the chances for them to be violated, resulting in UB), are different.
$ echo "pub fn my_function() -> i32 { 0 }" > bar.rs ; rustc --crate-type=dylib bar.rs && nm -A libbar.so| grep my_fun
_ZN3bar11my_function17hce21faeb92ac13c6E
$ echo "pub fn my_function() -> i32 { 1 }" > bar.rs ; rustc --crate-type=dylib bar.rs && nm -A libbar.so| grep my_fun
_ZN3bar11my_function17hce21faeb92ac13c6E$ echo "pub fn my_function() -> i32 { 1 }" > bar.rs ; rustc --crate-type=dylib bar.rs && nm -A libbar.so| grep my_fun | c++filt
libbar.so:0000000000047230 T bar::my_function::h4ed6ea856a52cd6b
So adding a single hash to the end of the symbol is a joke?
To be clear, my comment was about the fact that two different functions produce exactly the same symbol name, c++filt or not, which is not what I was told above in " It carefully designs its name mangling such that this doesn't happen in the first place, even in the presence of multiple slightly-different builds of a library."
I have no particular comments on the idea of using hash though I believe that something that changes 95% of chance error in 0.5% of error (I'd assume, as it took me 10 seconds to find a collision) is very bad - you want errors consistently when you fuck up, not once every hash collision as it sounds like a really really big pain to debug when it happens.
You cannot "copy" simplicity into a complex language. (Not trying to start a discussions about whether or not Rust is complicated.)
If Zig can settle in the niche that C is used for today (including "nearby" areas where C programmers consider switching to a higher-level language), then it is already a great success. I think (rather: hope) what we will see in the future is that no single language will dominate certain fields anymore like it was the case in the 90's and early 00's.
Rust doesn't need to fit into every niche, and it would be harmful to bend Rust in a way that it fits everywhere. It would end up as a "kitchen-sink language" with tons of competing concepts and ideas. This is exactly what's currently killing C++.
That niche is "code that needs to be portable to any system with a C compiler", or "... to a particular system that only has a C compiler" so i am not sure it can.
There are lots of projects using C that aren't in that niche, but i don't believe there is a good reason for any of them to be using C over C++ these days. It's either history, inertia, or Luddism.
That's the old (and frankly: tiresome) mindset that C++ is a successor and improvement of C. After using "modern C" (as in C99 or later) for a while it becomes quite obvious that this isn't the case anymore, instead C++ was a fork of C and developed into a very different direction (including developing the original C subset into a non-standard C dialect). Especially with more recent C++ standards, C and C++ have become different languages with very different goals.
It really isn't. It's based on comparing the languages as they exist today. C++ is vastly more productive.
We're not using string based macros, but hygenic macros.
We're not using goto error, but instead RAII.
We're not guessing if a function returns 0 or 1 on success and if the big struct we passed in as a pointer is valid on either of those, we're using an ADT that makes it clear.
Slices mean everyone aren't reimplementing 1000 morphs of the same buffer struct.
And the deeper separation between code and data that the fat pointers give you and how the more you go down OO principles, the less idiomatic it feels, really feels more like a giant C codebase than a C++ one to me.
In C one has to be very disciplined about this type of stuff and needs to put much more thought into module API design (for instance to enforce memory management rules), because the language itself is extremely "freestyle".
You would think that, but for example I think that, for example, Oxide should definitely have picked zig, if it were more ready. Obviously it's not, and oxide wants to ship now.
https://dlang.org/blog/2019/07/15/ownership-and-borrowing-in...
What about extremely fast compilation? In theory a complex language could have a really fast compiler, but I don't think there are any historical examples of languages with a very slow compiler getting compilation down to around one second as the article describes.
I have never seen anyone focusing so ruthlessly, so early, on nitty-gritty details of how to go about engineering a compiler and language that is and will let you be as close to optimal as possible in terms of compile-time and run-time performance. "Perfect software" as Andrew talks about.
In short, engineering choices taken early will let Zig be something that Rust maybe could approach too (in theory), but in practice never will. Of course Rust is something that Zig will never be, too.
I have seen many say just combine PL design innovation and tooling part and it will be perfect but I think this will not happen because it becomes very difficult and sensibilities of these approaches do not match.
It is just innovation mostly guided by practical engineering concerns. In this case: How to avoid requiring expensive heap allocation for async tasks, and how to be sure you don't crash from a stack overflow due to unexpected input for example.
I suspect that the excellent Zig comptime support is made possible by intentionally going for a pretty unambitious type system. Otherwise how could you soundly let arbitrary code generate a type? A fancy type system that wants to reason about that will fail, or at least cause the whole language to revolve around making that work.
I think you are right that it would be very hard for a language to innovate both here, and in the traditional PL-teoretical way.
I for one have a really hard time liking expressive ones, hence Go & Zig is my small but high-quality toolbox rather than a larger toolbox.
I just can't get that excited about the language itself beyond a certain point, I just want simple, predictable high-quality tools to help me produce simple, high-quality code and applications.
Anyway, all of the above is ofc subjective.
Good! Maybe you can actually get something done using it then, instead of getting lost in mastering lots of concepts just for the sake of it.