But what for a C programmer (systems/embedded) remains to be seen, is, if any of the new languages (say for example Rust, Zig, Odin) can (or want to) offer one of C's strengths most language designers do not think of (at least it looks to me like that). And this would be for me "leave the language alone (for the most part)".
If i may quote from Zig's website (https://ziglang.org/#Small-simple-language): "Focus on debugging your application rather than debugging your programming language knowledge."
I have yet so see a community driven language that fulfills this promise in a comparable way to C's. There's always "this little feature" necessary, or "this cruft to clean up". You name it i've seen it.
If you want people like me (and trust me HN community, there's a GIANT silent majority out there) to use Zig e.a. for production in the embedded and systems space where product lifecycles are counted in decades, you will have to provide this feature. Sure, you can call me a dinosaur (still under 40 :-)), or a BLUB programmer or what not. But language designers wanting to (slowly) put C to rest, should, imho, not forget about this "feature".
In this space you really want to focus on the real problems, not on problems of keeping up with the latest and greatest.
Side note for all the "if all you know is a hammer" responses: Please don't compare programming languages with tools. This is a bad analogy. Programming languages are more like materials. Once chosen and the product (the source code) built, you cannot simply change it without starting from scratch and a new product (new source code).
So, downvotes, here i come ;-).
C is not going any where.. we all know that. But it is not the best tool for accomplishing many systems programming tasks. IMO, this has probably been true since Ada95. Ada2012/SPARK, Rust, and probably Zig make this even more clear.
For some domains, C was the ONLY tool available... e.g. C is/was the only language with board-level support and an available compiler.
At my place of work, we presently use C for embedded code because it is best supported for the microprocessor we are using... but we'd rather use Rust, Ada, or Zig. we may explore using Nim to generate C with static-only allocation.
Rust has the (imo) above stated problem. Also, if you avoid dynamic allocation, Rust's advantages are not as compelling in the real world as advertised (personal opinion). They are there, but IMO minor.
The problem right now is that we have a chicken and egg problem. You said you work in embedded too, so you know MCU bugs exist. Use anything but the "recommended" tool chain, and the compilers will be blamed, no matter how much proof you have that it is the MCU.
Don't get me wrong, i'm definitely keeping an eye on Zig. There is lots to be liked. I cited that its goal is keeping the language simple. But only time will tell if the developers can keep up to this promise. I, personally, don't trust languages developed by communities in this regard. Reality has (sadly) proven me right.
(Are you talking about ide licences, the commercially supported version of the compiler, ramp-up time to productiveness, or maybe something else?)
Somehow I've become interested in the economics of Ada, and am considering it in comparison to several other strongly typed systems programming languages for a learning project. So I'm a bit curious.
Alas... not to mention debugging tools, etc. That's why I've gone down the Nim path at work as well, and it's actually pretty fun once you get the basics. Having JSON support in a few kB's of flash is nice. I've ported enough of the Nim standard library to FreeRTOS/LwIP to be able to use it with the ESP32! If you want to take a look and see how it integrates with FreeRTOS [1].
I'd recommend trying out Nim's new ARC garbage collector if you have more than a few dozen kB's of RAM. The ARC GC is pretty friendly to working with C based-boards as it can use the native malloc and any native debugger mechanisms. The references are non-locking to it's pretty low overhead, but if you do try it run `-d:nimArcDebug -d:traceArc` first to see the overhead. You can't use `async` without cycle collection (ORC) however.
Of course no-GC works too and you can annotate functions to guarantee no GC (I think). Just less fun if you have the RAM available.
1: https://github.com/elcritch/nesper (Warning: WIP)
It's currently much more expensive to port an Ada, Rust, or even C++ toolchain to a new platform, so I'd expect C to continue to be dominant on lower-spec chips. Here's a previous comment adding a bit more technical detail to that argument: https://news.ycombinator.com/item?id=22822931
I have to say, zig really captures this feel. The only places where it gets a tiny bit strange in the way you complain about is comptime, but imo it is a very good trade-off for ditching the preprocessor and ditching make. And anyways comptime is far more easy to reason about than some of the mangled ways in which people who use C (in prod) use preprocessor: It took me weeks to understand the ffi header file for the erlang VM, for example. Three languages for the price of 1.25. not bad.
As an example of this one of the features they added was NLL (non-lexical lifetimes) which was designed to help iron out some of the annoying places the compiler couldn't see what you were doing was safe. One of the constraints they gave in implementing it was no changes to existing code that compiled, the way they did it as a non-breaking change was to ensure that the new system only allowed programs to compile that didn't compile before.
It sounds like you aren't interested in Rust for other reasons but wanted to point out that language hears your concerns about breaking changes being the worst.
Also, editions are no response to the issue of new features being added to the language, changing what is considered idiomatic, okay, etc. If Rust turns out to evolve constantly like C++ has in the past, then it'll be hard to keep up with it, in addition to the great one time cost of learning it in the first place.
Basically the idea was rather than making it as easy as possible to stay on the oldest version they focused that effort on making it as easy as possible to update once you decided that was what you wanted to do.
This one is tricky for a few reasons that don't apply to most things.
Especially after NLL landed on 2015 ><
If I am inspecting code for safety that is doing low level things then I want to have a clear and simple (imperative) model in my head. I don't want to spend the review admiring how clever the author is. I want to know as directly as possible what is going on in the machine.
In another domain the ability for the author to do clever things could make me very productive but it doesn't matter because the safety of low level code is not a concern.
C++'s `unique_ptr` is a "newfangled feature" but is not the latter (ignoring custom delete functionality).
I tried to account for the discrediting of zero cost abstractions (which I buy) but I think you are adding the latter onto that which I didn't get from the person I was responding to.
> I want to know as directly as possible what is going on in the machine
Certainly not in multi-threaded code on an x64 machine (and IIRC ARM is trending in that way as well). There are too many abstractions to actually do that, the best you can do is understand the abstract machine of the language and the rough edges translating that into real machine code entails.
> safety of low level code is not a concern
I will remind you that in the case of Rust safety is where all of this comes from not productivity.
Writing a mutex by hand is hard and you will make mistakes, here is a library construct to do it for you. It will be more complex than what you would write but it will be safer because others have worked with it. etc. etc.
When working with minimal computation resources using "works for everyone" constructs stops being zero cost and starts being expensive both conceptually (what were the poisoning rules again?) and performance wise when the cost of providing your abstraction is hard (why can't I arena allocate an RC?)
Those are the situations where Rust and even C++ have difficulty shining and C wins out. It provides so little you never have to pay for anything but what you absolutely need. But remember the things you lose out on are from every category you can think of, not just one. Performance, reliability and safety are all often sacrificed in some amount when writing custom code.
When you write code instead of a random Joe in a garage you know who to blame when it blows up. When you write code instead of using the larger community code you give up the often great characteristics in every metric for your more fine tuned characteristics in some metric generally.
Sorry that got wordy... TL;DR nothing about most low level languages up to things like Java and C# IMO stops you from having a simple imperative model in your head so using that to chose C over something else is missing the point.
On the other hand if you change the language, say every two years, when is the right point to dive in? Is it at some point (say i missed the last four years of language development) hopeless to catch up?
I mean, this is only true if you ignore the myriad ways in which C allows you to manifest memory unsafety and undefined behaviour
"I mean, this is only true if you ignore the myriad ways in which C allows you to manifest memory unsafety and undefined behaviour"
Which Zig has as well in release mode. "Yes, but in Debug/ReleaseSafe it's much stronger than C with sanitisers."
Is it also stronger than C with Frama-C?Are you sure you hit all the execution paths during development?
Pervasive casts? Not under MISRA C.
Look, I'm not criticising Zig on a technical level (I mentioned above I'm keeping an eye on it because it looks interesting), but I don't have any trust in community driven languages keeping it simple (although Zig promises to do so, see link above).
EDIT: Corrected formatting.
No, and, in fact, I specifically said that C has unparalleled tooling in the formal verification space: https://news.ycombinator.com/item?id=24618856
But, 1. as much as I like formal verification and am actively involved in the space, we're talking less than 1% of C code that uses those tools (unfortunately), and 2. because Zig is so simple -- simpler even than C from a formal analysis perspective because of slices and casts -- if it ever becomes successful, I have no doubt we'll see such tools emerge.
Of course, the other 99% can't use Zig either because it's not even "out" yet. Obviously, a product that doesn't even exist yet can't replace any product that does, and no one is suggesting that Zig -- in it's current virtually nonexistent form -- is a replacement for anything, let alone for C. We're talking about what Zig could do when it exists in some production-ready form.
I think Rust is a great, worthy language, and for a lot of use-cases it makes sense to optimize for trying to just put everything on the stack, but there are a lot of other cases where what you really want to do is own allocation by yourself instead of trusting it to the compiler. I haven't tried Zig, but I really want to see if it can deliver on this.
The new test allocator checks your code for memory leaks, use after free, and double free in a highly ergonomic fashion. I think is fantastic that zig encourages writing tests, and I think if you get full test coverage in your zig code with the test allocator, you will probably solve 99% of your memory errors.
I know this is possible in C++ (see alexandrescu's allocators talk from CppCon 2017?) But I would be surprised if anyone does it.
With zig, not 100% comparable (like not sure about gdb, but zig stacktraces and error traces are fantastic) it's all done in one language. Such a lowering of mental overhead and entry barrier!
The only thing you have to learn is that at comptime you get access to some magic (like the ++ operator)
The only hard part is figuring out how to pass -fsanitize=address into your compiler via your complex build system! And building with symbols.
That is inherent to the C/C++ toolchain and not a problem that sanitizers can address.
A lot of the advantage which Zig has over C is exactly in not having to support a towering edifice of tools and hacks which is older than I am. This is true to a significant degree of Rust and C++ as well.
This could be done in Rust. There is, for example, the simple bump allocator bumpalo [1].
It would be nice if the the std collections supported this (in planning, but hasn't seen much progress), and most dependencies would not be built around a manually passed allocator.
> Rust assumes most of the time you'll be passing around references to values or small structures allocated on the stack.
Can you clarify what you mean here? All of the std collections (Vec, HashMap, etc) use allocation. There are also `Box`, `Rc` and `Arc`, which allocate and are used everywhere.
Again, I do not think this a weakness of Rust, and I do not think it should change. In very many cases, the Rust approach is very helpful.
The point is there are also cases where imperative, explicit memory management is desirable.
I agree that is the case, and Zigs convention of passing an allocator around and making those calls very explicit has merit.
I think you are over-estimating the amount of "magic" in Rust though.
The allocation-related logic is not part of the language or the compiler, but comes from the std (or alloc) crate. You just don't usually see a "malloc" call because it is hidden behind types in the standard library, which default to a global allocator. You still "manually" allocate by calling eg `Box::new()`.
The only magic is that Rust calls a destructor ( Drop::drop) when a value goes out of scope. Types can implement drop and use it to deallocate or decrement a reference counter. This is made possible by using an affine type system with lifetimes and move semantics.
This is conceptually not that different from doing something like `defer allocator.free(x)` in Zig, though less explicit.
ps: I realize that conventions make a difference. But nothing in Zig would stop me from having a global variable with an allocator, and using that without passing it around.
That's correct and you probably should do that in your own applications, but it will be frowned upon in libraries, and people will not choose to use them if you make that choice.
There's also another pattern, where you stuff a pointer to your allocator in your struct and have your "object" carry around its allocation system, you don't have to pass around allocators that way.
And it doesn’t have to be just one allocator either, you can easily use multiple allocators simultaneously.
Can you expand what you mean here?
I really like Zig, and there is definitely a big design space to explore in creating a modern low-level language that doesn't come with the complexity of something like Ada/Spark or Rust.
This binary patching + daemonized compiler approach is particularly exciting.
But as a language, I'm not aware of any features that would amount to a "radical breakthrough". As far as I am aware, it is a modernized, low level language, with nice compile time evaluation, async, and some runtime-provided safety guarantees. But nothing that is so novel from a type system / language design point of view.
Happy to be corrected though!
I'm not sure I like the "better C" label. After all, Zig allows you to easily do stuff that's virtually impossible in C, and only possible in C++ with templates and constexprs and concepts. It also doesn't have pointer arithmetic and pervasive wild casts, so it's not a "syntax-sugared Assembly". Its only similarity to C is that it is a low-level language and that, unlike other low-level languages that aren't C -- like C++, Rust, and Ada -- not only is it not among the most complex languages in the history of software (all three of those have probably secured their places in the top-five), but it is probably among the simplest.
> I'm not aware of any features that would amount to a "radical breakthrough". As far as I am aware, it is a modernized C, with nice compile time evaluation, async, and some runtime-provided safety guarantees. But nothing that is novel from a type system / language design point of view.
AFAIK, general partial evaluation with introspection as a single mechanism to do the work of generics, typeclasses/traits/concepts, value templates, macros, and conditional compilation -- combined with a general error reporting mechanism that is shared between runtime and compile-time -- has never been attempted before. It is revolutionary.
I agree that Zig is more than a "better C", and I edited my comment.
Regarding `comptime`: I haven't fully grokked it, but it looks very similar to the capabilities of D (and in part Nim). Do you by chance know those languages and can give a comparison?
(Languages like Idris also allow to express a lot at compile time, but that's quite a different domain)
(To be clear, I don't use Zig, yet.)
Have you seen http://terralang.org/
BTW, even Zig's build language is just Zig!
Fully optimized release builds will surely still have an option to use LLVM
So, zig is new to me but looking at an overview of the feature in question here this feels pretty hyperbolic? I certainly agree that the approach it takes is syntactically less noisy than angle-bracket generics/templates (which I dislike quite a lot) but it doesn't seem particularly revolutionary vs. the way evaluation of types in haskell works, for eg. A function that produces a type based on type arguments is roughly a typeclass.
And the intermediate products of a function with comptime arguments are still pretty much semantically equivalent to a C++ template anyways, just with the arguments in a different place.
It is cool, and I like the angle it takes, but it seems more synthesis than revolution to me. Am I missing something?
I guess I should also mention my inspiration, the C++ dyno library (https://github.com/ldionne/dyno)
I don't really see how this is relevant to typeclasses though ^^
All that said, I’m personally okay with the decision to omit typeclasses since the language is meant to compete with C, not C++.
I'm sure there are a good handful of other fields that currently write "minimal C-like C++" and I think Zig competes with C++ in those cases.
I don't know about that. "that of C's "syntax-sugared Assembly" is pretty much how most people want to do system programming. Don't systems programmers want to be as close to assembly without delving into the pain that is assembly? Also, I don't know how "ground up" it is. Seems like zig is just more syntactic sugar on top of C to me.
Wasn't there a big push for D to supplant C/C++ a while back. Now Rust has joined the fray. Why would Zig succeed when D failed and Rust is failing? What is it that Zig offers that D or Rust doesn't? And does that justify the migration of legions of C programmers and mountains of C code to Zig?
It manages to address all of C++'s biggest shortcomings, which, in my view, are 1. language complexity, 2. compilation time, 3. safety -- in this order -- and it does so in a language that is arguably simpler than C, can be fully learned in a day or two (although the full implications of the design might take longer to sink in), and also inspires a new approach to partial evaluation, replacing generics and value templates, concepts/traits/typeclasses, constexprs/procedural macros, macros (or, at least, the "good parts" of macros) and conditional compilation with a single, simple feature.