> This year, I decided to look into Zig. Both Zig and Rust are more or less in the same space, replacing C.
Maybe we should think about this in terms of space occupied by the languages and not the language themselves. One of the features of C is that pretty much anything can interface with it. Another is that you can write very fast code with it. Another is that pretty much any platform has a C compiler. Another is that it's small and fits in your head. Depending on what you're trying to do, you might use one "C replacement" or another.
Another way of looking at it would be to see the shortcomings of C you're trying to overcome. I think this way of thinking would be more productive than just thinking about "replacing C", "replacing C++" or things like that.
Zig libraries can be exported with the C ABI[0], so pretty much anything can interface with it. You can write very fast code with it. It is also small and fits in your head. The only thing missing is a variety of implementations, however there is a clear path to bootstrapping the compiler from source with a semi-recent C++ compiler[1] and from there, there's already a growing number of supported targets[2]. There is also a major focus on making sure cross-compilation is well supported out of the box, including C code that Zig can also compile[3].
It's perfectly fair to compare Zig to C in this case, as the project originated as a dialect of C before the author decided to do a complete clean break[4]. The primary frame of reference when designing the language was C and addressing its shortcomings, both language and tooling.
I'll restate my point: sometimes when replacing C people want something that's just as fast but also way more safe, and in that case will usually reach out for Rust. That's what the people doing the cryptography in Python did. Rust can also expose a C ABI, you can write code as fast as C. People will often choose Rust instead of Zig because Rust has stronger guarantees about memory safety, and these guarantees are more important for them than having a language that's small and fits in your head.
This is why I think talking about what you're trying to achieve is more fruitful than just talking about the language you want to replace. For example, if you want to replace C code and you don't need all of the performance in the world, you couold simply switch to a garbage collected language. In that case, since speed wasn't the most important requirement, a language that isn't usually thought as a C replacement can replace C code.
If you want to make the point that Zig is "closer to C" compared to other language that wants to replace C, that's a fair claim to make.
Rust is also not 'small and fits in your head', so that's yet another point it is missing in your initial set of criteria. Its main implementation and library also emits rather large binaries in the usual case. You may not find this important, but others do.
> People will often choose Rust instead of Zig because Rust has stronger guarantees about memory safety, and these guarantees are more important for them than having a language that's small and fits in your head.
For some people, having a language that's small and fits in your head is not a requirement, not a priority, and sometimes not even important.
As others have pointed out, neither is C if you take into account avoiding UB, the preprocessor, the build systems, the many stdlib functions you need to avoid, etc. You might already know how these work, but that doesn't mean there's less to learn than a language like Rust.
> This is why I think talking about what you're trying to achieve is more fruitful than just talking about the language you want to replace
This is definitely the right approach in my opinion. Even for your garbage collection example on the one hand, in some cases your program is dominated by tricky reclamation problems and the "correct" Rust program is basically 90% garbage collector, so that an existing GC language might produce faster programs; but on the far opposite scale maybe your program has tricky non-memory resource problems and GC doesn't help at all, so the GC language has worse performance and yet it makes your problem harder to solve than in Rust anyway.
One of the nice things about Rust and about C++ RAII here is that you don't care whether the resources are memory. Database handles? Aerial drones? A tape robot? These are all just resources you can wrap in an object to manage them, in both languages. "I only have 15MB of RAM" and "I only have 4 fire-fighting robots" are the same kind of problem from this perspective.
> If you want to make the point that Zig is "closer to C" compared to other language that wants to replace C, that's a fair claim to make.
This is really important. Zig is very much inspired by C. Rust has the syntax of a semi-colon language like C, but its inspirations come from all over the shop. I came to Rust having already programmed not only C and Java (and a little C++) but also Python and ML (and a little Prolog) and so this was less disconcerting. But I can see if you've literally never encountered a functional language some of Rust is weird because even though it is spelled like semi-colon languages you know it behaves very differently.
I like Rust, much more than I liked Go or Python when I picked those up. It feels less clunky. An elegant weapon, for a more civilised age.
For the comparaison between C, Zig and Rust syntax, I'm honestly not that sure. People love to complain about Rust's syntax for reasons I don't really understand. Sure the turbofish is a bit weird the first time, but other than that I don't find it especially difficult. On the other hand, I still have a hard time understanding Zig code. Probably because I never really played with it, but it's just like Rust, you need to familiarize yourself a bit. Maybe people that did only C are able to get up to speed with Zig more quickly than Rust? But that seems a bit superficial, if these people are already spending most of their time with only one language, if they learn a second to spend that much time with a few hours to get up to speed with the syntax is nothing.
I'm really curious to hear any stories or more detail about how Rust can feel weird to someone without experience with functional programming concepts?
One of the things I like most about Rust is how aggressively normal it feels to me, and it's really hard for me to remember what it's like to encounter this stuff for the first time.
Absolutely! A common example I like to use is Java. It might be hard to imagine now, but there was a time where Java (and then C#) were pitched as a replacements to C++ (even the name "C#" supposedly derived from "C++++"). There was no reason to write native code any more, many people said. And, they were kind of right. Many people were writing applications in C++ because they thought they had to, even though they weren't writing C++ in the style needed to make proper use of it. A lot of old C++ code bases today look like much more verbose Java applets. This peeling, when it happens, is usually good for everyone since languages (and tools) evolve to better fit the domains in which they are most used and happens so gradually that few notice. The inverse also happens, but with more conflict: you can see this today in Python (and other dynamic languages) adopting typing features aimed at assisting tooling for large code bases, well beyond the traditional learning or glue scripting mentality for which they were designed. This often results fierce debates around language features (like static typing vs dynamic typing) that are really proxy battles for the usage intent of the language.
Unfortunately, I think the "replacing X language" mentality happens because it's the easiest point to start with. The field of programming and computers is so vast now that most (all?) people have a very narrow view and it's easy to assume it's universal. I've lost track of all the "things all programmers should read/know/do" articles.
> Unfortunately, I think the "replacing X language" mentality happens because it's the easiest point to start with. The field of programming and computers is so vast now that most (all?) people have a very narrow view and assume it's universal. I've lost track of all the "things all programmers should read/know/do" articles.
I think another effect is that if you really like a language, promoting it is one of the best thing you can do to work with it. When you see the recent (2010 and later) new languages, they all have a story of addressing a particular pain point. Typescript is Javascript at scale, Kotlin is old Java on android, Go is web services, Rust is performance and safety. This is solid marketing, and is part of how these languages end up dominating their domains. On the other hand, a language that's "X but slighly better everywhere" won't really attract people, since there is already code and jobs in that language.
Currently in the Great War Of Replacing C And C++, there are lots of fighters. Some people promote Rust, some others Go, some others D, some others Zig, some others to just keep writing C and C++. Part of the battle is writing code, libraries. Part of the battle is making the language and tooling itself better. And part of the battle is making articles, comments, talking to people and promote your language.
The best way to win battles in the context of systems programming languages is to have a platform ship with the language, libraries alone won't do it.
This is how C put other of business other system languages, much better than it. When you get UNIX and have already paid for a compiler, hardly any department will allow to buy additional compilers.
And C++, people tend to forget it was also born on the same place as UNIX and C, thus it was quickly adopted by all OSes that were UNIX clones in some fashion, and then by C compiler vendors on other platforms.
And on cases like Java, Go, TypeScript or Kotlin, you need a godfather company to push it no matter what.
For the part about platforms, hasn't that ship already sailed? From what I understand the industry had settled on C, C++, Windows and UNIX. Some people are trying to replace some of these with different solutions, but that's about it. I think a new platform would have the same challenges as a new system programming language, but would be even harder to convert people to.
Depending on which level of the stack, the painless option will be C++, Objective-C or Swift.
On Android it will be a mix of Java (slowly being left behind on purpose), Kotlin or C++.
On Windows, .NET (and in some cases C# or C++/CLI only due to the way MSIL gets exposed) and C++. You can also keep using C if your view of Windows is like Windows XP APIs.
On Fuchsia, it is all about Flutter, C++, Go and Rust.
On ARM Mbed and Arduino, C++.
On microEJ, Java and C.
On Meadow, .NET.Core.
Azure Sphere, C.
CUDA, C++, Fortran, Python JIT and PTX aware backends.
There are plenty of other platforms I could keep on listing.
Naturally you can try to fit something else, but then it is on you and others to build the ecosystem, work around support issues and lack of IDE tooling.
Many C++ code bases, especially older ones, I have seen were really trying to be their own Java. This caused them to join disadvantages of C++ with disadvantages of Java. I don't believe the result was faster than JVM and start up time hardly mattered and still was not fast enough.
I think there's an implicit, often unspoken list of C shortcomings wherever these replacement languages are concerned. Some of the most obvious ones are: Macro hell, transitive include hell, and the ease with which even experienced developers can unwittingly create unsafe code.
> Another is that it's small and fits in your head.
I know this is a common view, but I want to highlight some counterexamples:
- C programmers need to learn about many different forms of UB: https://blog.regehr.org/archives/1520. Most take years to learn these rules. Many never learn them.
- Most real world C programs use some nonstandard language extensions on some target platforms. C learners trying to read production code have to navigate these.
- The C compilation and linking model is complicated and platform dependent. Compilation is a major stumbling block for students who have to learn Make or CMake at the same time they learn C.
There is some truth to the "C is simple" idea, I'll grant that. It's obviously simpler than C++. But I think there's a big effect where we tend to forget how hard it was to learn, because we learned it so long ago. There's also been substantial progress since the 70's on designing simpler programming languages.
That's a good point. I personally don't share this view, and it's also not something I look for in a language. But people using C currently seem to be very attached to this, and knowing how to connect to your audiance is important.
A reduced feature/keyword set does not a simple language make. C is smaller, but definitely not simpler. Also definition of simplicity is context dependent, I would say in many scenarios C++ turns out to be the simpler choice.
Thanks! This is probably the first time I've heard C called not a small language.
Perl was my main language for over a decade, and when I wanted to go lower, I honestly struggled... but not with the language itself, with the peripherals which are exactly what you described!
C is not a difficult language. I know a lot of electronic engineers that know how to program in C but don't know a thing about informatics, for example what is DNS, or what is JSON, or how to access a FTP server, and things like that. But they know how to program their microcontrollers in C.
> C programmers need to learn about many different forms of UB: https://blog.regehr.org/archives/1520. Most take years to learn these rules. Many never learn them.
UB really don't care that much in real life. When you are targeting a particular platform, that for most C programmer is some sort of 8/32 bit microcontroller, the only thing you care is the datasheet of the microcontroller. Of course you do things that in general are undefined behavior, but in the particular case lead to a perfectly predictable result.
In reality you only have to care about UB if your goal is to share code between hardware platform. But let's be realistic, most of the time you don't. Even if you do, some UB are these days standardized, for example integer overflow is UB, in practice you know that on every architecture 2 complement arithmetic is used and thus is well defined.
The real problem is that compiler for a misinterpretation for the standard started treating UB as "you are allowed to do whatever stupid thing you want since the condition will never happen", that is the real problem. In reality if you compile without optimization and with flags that avoid this stupid decisions it's not a problem.
> Most real world C programs use some nonstandard language extensions on some target platforms. C learners trying to read production code have to navigate these.
Again, it's a problem only if you are targeting different hardware platforms. But if you only program on ST microcontrollers (for example) why should you care?
And you must use them in some situations, for example in C there is not a way to control how a struct is packed (to decode efficiently data from a serial connection for example you define a packed structure and access individual fields). Or inline assembly is not in the standard (what if you need to use an instruction of the microcontroller that is not exposed by the C library?).
> The C compilation and linking model is complicated and platform dependent. Compilation is a major stumbling block for students who have to learn Make or CMake at the same time they learn C.
It's. But most of the time you stuck on whatever the development environment for the target hardware platform is setup to. Most C programmers doesn't even know what CMake or Make is, I care about it and do things by hand, most other people use IAR or Keil or whatever proprietary software that integrates compiler, build system and IDE.
I think your post highlights perfectly why I think we should talk about the roles of C and not C itself. The C you talk about seems to be a very different beast compared to the C people write and use when building shared libraries that are used across a wide variety of platforms. For example, the people writing OpenSSL would have very different concerns from the people you talk about.
Compilers are allowed to do whatever they want when encountering undefined behavior, not because it will never happen, but specifically because it’s undefined.
> In reality if you compile without optimization and with flags that avoid this stupid decisions it's not a problem.
Worse programs. Written by people who believe "C is not a difficult language" and who sacrificed all benefits of using a high performance language because they couldn't understand what they were doing.
So long as you don't have any competitors who use a better language or skilled programmers you can coast along like this, putting out a sub-par product and just pretending it's the best possible. But you can get a very sudden and rude awakening if anybody decide to offer what's possible here.
I think you should be a bit more precise on exactly when. If it compiles to C, that means that it inherits from C the great platform support. I don't know much about it, so I can't judge Nim on other qualities that people attribute to C: you can call it from pretty much any language, it's seen as simple, you can write highly performant code in it, you can use it in very restricted environments, and I'm probably forgetting some others.
Honestly, I don't think these kind of drive-by shallow comments are selling anyone on Nim. Many people here have posted long and insightful comments about the complexities of replacing C and some other topics, and two of the three comments about Nim are a bit shallow.
I’ve seen the “clean up errors yourself” argument before, but, like the author, I don’t think it holds water. Often the correct response to errors is to panic() or pass it up the stack so the caller can deal with it or—more likely—panic() itself.
I think it's nice to have two mechanisms for errors like in ML: sum types for errors that the calling function can realistically recover from, and exceptions for the rest.
Yes, and you can even handle some errors, and then capture an error whose type is the subset of unhandled errors, and return that, limiting the error set of the current function to not include the handled ones. Here are a couple examples: https://github.com/ziglang/zig/blob/7d0de54ad44832589379a4bc...
You get compile errors if you try to handle an impossible error, or don't include an `else` prong (in which case the compiler tells you the full set of unhandled errors).
This is Rust's approach, too: realistically recoverable errors are handled with Result<T, E>, and other are handled through panics. (Panics are generally implemented as exceptions, i.e. they unwind the stack, but the compiler can also be configured to make them just abort.)
I agree with the author that errdefer seems like the winning feature of zig. Proper error handling is the most important part of any language or system.
Unfortunately most common languages does not prioritize this. As a contractor I have dug myself thru a lot of awful code in different languages and the most common denominator is improper error handling.
When I was reading the original blog I was thinking if he had any follow up, so I decide to click on archive and the homepage. And this article, somehow doesn't show up in both list. As a matter of fact I couldn't even find how to get to this blog post without your direct linking. Then it turns out it is a "FUTURE POSTS".
How does that work and why? Is this suppose to be some sort of preview before it is officially published?
Author here - I typically just throw things into the post queue and they drop every day.
In this case, I replied to a question and it turned into a full blown post.
Purely on the topic of `errdefer`, I believe the equivalent in Go would be:
// must use named returns so you can observe the returned error
func yourfunc() (result thingtype, err error) {
thing := constructIt()
defer func(){
if err != nil {
thing.Close()
}
}()
// use thing to do a few things that could error
err = thing.setup()
if err != nil {
return nil, err
}
other, err := thing.other()
if err != nil {
return // naked returns are fine too
}
// etc
return
}
Which is...... technically possible, and not even all that bad (as much as I strongly dislike named returns, since I find them very error-prone), but the fact that I have not ever seen this done does sorta reveal how much friction there is. Instead, I usually see manual additions of err-cleanup to relevant branches, and I've seen that pattern fail repeatedly as well (especially when the code is modified much later by some other person).
The Zig equivalent seems like it would be roughly (I don't know Zig)
func yourfunc() thingtype {
thing := constructIt()
errdefer thing.Close()
// use thing to do a few things that could error
try thing.setup()
other = try thing.other()
return thing
}
which is quite a bit more palatable and future-mistake-resistant. A sizable chunk of that is due to `try` of course, but still.
I'd be interested to see if someone who works regularly with Go can show a cleaner way to do this. Because what I'm seeing here is just reinforcing my biases against Go. I've tried Go a couple times before but only ever read about Zig and yet I still find the Zig far easier to follow. It's much less verbose without being less explicit.
(Also I'm not sure if the compiler would catch this, but there seems to be a bug in that Go code that's hard to see at a glance. You're never assigning a value to result)
I work regularly in Go - for an isolated example, that's about as clean as it gets. If you want to make it a little more middleware-y you could do this:
func iferr(err *error, f func()) {
if err != nil { f() }
}
// use
func whatever(...) (err error) {
thing := ...
defer iferr(&err, thing.Method)
}
But that only works with an exact match, so that example can't match e.g. `Close() error`, so you still have to either make a ton of near-clones for different signatures, or use a ton of trivial closures (like with the manual version), to make the type system happy.
Which can be worth it in a large enough project - `func()` and `func() error` will take care of like 90% of the signatures you'd defer anyway. I'd probably do that personally, unless I only needed it once or twice.
There has been a somewhat similar check/handle proposal (close to "try"), but it has consistently fallen into "they're great for trivial cases, but they make it harder to have good error messages / wrap errors with additional context", which is absolutely necessary in Go since errors are so loosely typed and contain only explicitly-added information (no stacks by default, for example). So I'm glad they haven't happened yet.
Ah, and: yes! You're right, it does fail to return any `result`. That was unintended, and as-written it won't complain, since `thing` is "used" (methods are called on it) and `result` does not need to be used: https://play.golang.org/p/iSU5YWyxODC
One thing that I’m quite bummed out with Zig is the lack of a good http server/client implementation in the standard library. Golang’s std implementation on this is really quite nice.
Do you feel like http server/client is ‘mandatory’ in a standard library in 2021? I don’t really do any web adjacent programming so it’s not something I ever think about. However, I know I am in a very small group of people who don’t have to worry about web stuff. I’ve been toying with a release of the personal language I use and was just rolling around the idea of what to put in the ‘standard’ library if I do release it. Anything else you think has to be in a std library?
It's very much a nice to have for a new language nowadays, as even if web dev isn't the key niche of the language, any success the language has is likely to still include a need for it. Adding it also creates interfaces and conventions that any third party ones can choose to adopt, to ensure it's a drop in replacement (and even if they don't fully, they'll likely still follow the same paradigm). This is really beneficial as, while the stdlib's version may be very middling, it ensures adopting the language and a third party's http implementation isn't going to trap you into something that becomes unmaintained, and which doesn't have a clean migration path away from; you can always fall back to using the stdlib's http library, since there's an expectation it will be maintained along with the language.
That said, if you're putting your language up as a "see the neat thing I did that's helpful in this very specific domain", and not general purpose, with the intent of supporting, it's probably immaterial to you.
It’s less a specific domain thing and more an overly opinionated exploration framework for me. Not that it’s really important, but the language is a modal dependent type theory with a compositional (even though most people would call it concatenative) syntax that compiles to C, C++, and JavaScript. So I can always just reuse the underlying platform http and the rest of the stdlib.
I don't think zig is aiming in that category. I see it as a system language.
Although these days people want to apply their favorite language to everything, I don't think zig will be a main language for writing web backends or http services. it's place is game engines, computer graphics, kernels, drivers etc..
Basic string manipulation, JSON encoding, file reading/writing, some type of hashing/checksum, and an HTTP client/server. Those things I’ve really appreciated about Go lately as I get it to control robots (controlling them over a REST api on local network writing commands to a serial port).
> Do you feel like http server/client is ‘mandatory’ in a standard library in 2021?
A server isn't necessary, but 100% yes for a client. There's a lot of eating your own dog food needed for an http client, like string handling with encodings, network bindings, various collection types, async I/O and/or binary streams, error handling. The resulting client showcases the strengths of the language and its standard library, and most data is on the web today.
High level bindings to cURL would be better imo. You don't reinvent this tricky wheel, and you get a lot of features (even async I think?) on top of a strong implementation. And a http server really doesn't belong in the stdlib of a system language.
Yes, a large team of highly paid engineers at Google produced better libraries than that one guy working on Zig as a passion project.
And yet that one guy still managed to build in useful features to his language that the Google "geniuses" couldn't think of despite (or because?) decades of experience.
Probably has something to do with Rust being used in embedded and kernel operating systems due to it's high level of memory control and zero runtime capabilities.
Rust is already used as a C replacement in some cases. However, as I said (https://news.ycombinator.com/item?id=28094998), just talking about a C replacement without mentionning why you were using C in the first place and what you value in a replacement isn't really productive.
Rust needs decades to catch up with domains that C++ has managed to keep for itself, like GPGPU, GUI composition engines, machine learning, HPC.
The other domains where C++ was strong for general purpose applications have been taken over by managed compiled languages like Java and .NET, which anyway make use of C++ libraries alongside their AOT/JIT compiled stacks.
There is little value to add from Rust in such scenarios, e.g. it won't make my WinUI application, CUDA shader or weather simulation model, any more safer than they already are.
Rust is a C replacement not in the "tactical" sense of "I like C but it's a bit shit", it's a C replacement in the "strategic" sense: it aims to displace C in most of its core usages e.g. writing low-level or portable libraries, CLI utilities, daemons, kernel and kernel modules, etc… places where only C (and C++ somewhat more debatably) would be an option and continuously causes issues.
It also aims to displace C++ when that's used in the same space, but in the sense that that's ground C++ gained from C, well C is the real target, if it can displace C there it can quite naturally displace C++ as well.
Every time I read about Zig, I feel like I see a nice language that's only an incremental improvement to C mostly in terms of ergonomics. It's hard for me to imagine people giving up C for only incremental gain.
An alternative perspective is that you don't have to give up C to use Zig - they are extraordinarily easy to mix together in one project. So you can incrementally move an existing project to Zig without rewriting the world.
Compile type computations, trivial cross-platform building and linking, vastly better error handling, and more. Honestly, I'm having a hard time seeing how these are only "incremental" improvements over C.
You could argue the opposite and say that because it is incremental and easy to mix with C, it will be more easily used.
When I program on my own I often write an idea in C and then convert it to C++ when complexity rises, precisely because C++, in terms of syntax, is an incremental update to C. Using say Rust would entail a more serious rewrite.
Unfortunately Zig and Rust are quite different in syntax. I would love to change my workflow from C->C++ to Zig->Rust but the transition is not as nice.
I thought that too in the beginning, but the comptime features, type reflection and error handling really go beyond C, while still being a simple and elegant language (in a way even simpler than C). Also Zig is a nicer C/C++/ObjC toolchain than most C/C++ toolchains (because of simple cross-compilation and integrated build system).
The important part is that (unlike C/C++ headers) included modules can't be polluted with code that's coming before the @import(). Everything else is just an implementation detail.
@import also has a second mode where the string isn't a Zig source file path, but a package name. AFAIK currently this also just resolves to a package-root zig file, but it could just as well be extended to some sort of binary format, the question is just: why?
Because many business situations we only want to distribute binary libraries.
If I want to distribute source code I would be using scripting languages.
It is also a nagging point for me when I play around with cargo, which contrary to conan and vcpkg, doesn't do binary libraries.
By the way, C++ modules might get a binary format definition by C++26 (too little time to sort it out by C++23), based on BMI/IFC formats already being deployed in GCC and VC++ respectively.
In which case, you can simply use the binary. Zig make is easier to consume external code, why have a separate process for consuming binaries from Zig vs. everything else?
To do this in Zig you would ship a binary + a "header" file that looks like:
`pub extern fn foo(c_int) c_int;` etc.
then import with `usingnamespace @import("./my_header.zig");` and now you have a function `foo` that you can call; then you just need to link with the object file that defines it. Is your complaint that you still need the "header" file? What language / tools do you use that let you skip that part?
Great article. I mostly do PHP, PHP has support for FFI and I have some private projects where I call C libraries directly over FFI. I have been thinking of using zig to implement my own C libraries when needed. Zig looks like a more sane approach than using C, another alternative would be D.
Zig looks overall interesting to me, but I don't like that it reproduces one of the pains of C++ templates, i.e. their dynamic typing. What I mean is, C++ templates aren't checked before being executed, and instead you get compilation errors (at c++ templates runtime) which mangle the use and definition of templates, e.g.
template<typename T>
concept HasId =
requires (T a) {
T::kId; // "the expression T::kId is a valid expression that will compile"
};
// A and B are expected to be structs, where each has an associated integer constant kId and a field "int value".
template <HasId A, HasId B>
int min_id(const A a, const B b) {
if (A::kId < B::kIs) {
return a.value;
} else {
return b.value;
}
}
struct A0 {
static constexpr int kId = 1;
int value;
};
struct A1 {
static constexpr int kId = 2;
int value;
};
// etc.
struct A180 {
static constexpr int kId = 2;
int value;
};
int main() {
(void)min_id(A0(), A1());
(void)min_id(A1(), A2());
// etc. ending with:
(void)min_id(A179(), A180());
}
And now you get 180 errors about your typo (if not for max error number limits in the compiler), when really there is only one error in the definition of min_id. I used concepts to show that in the long tradition of C++, they're an additional feature that doesn't really help (quantity over quality of features).
Haskell solves that with typeclasses, Rust solves that with traits. What's really important about these solutions is that when you write a function generic over a trait or a typeclass, you are limited to using what is defined in the trait/typeclass. The trait/typeclass function both as documentation and as something that gets you better compilation errors.
"typename" in C++ is basically "any", but for values of template execution. Typeclasses/traits on the other hand are a sort of types for those values (which we normally call "types"). And similarly, in Zig there is an "any" type for types, just like in C++ --- it's called "type", and AFAIK there is no way to constrain your types with types of types.
Is the problem about C++ templates it's error messages or not being able to strictly type the arguments in type signature?
I have a question to the compiler / IDE people here. In case of C++ templates, is it possible to output one error like: eg "class UserData needs to implement operator< to be used in min() function" instead of 200 lines errors? I mean, complex cases it's not possible but at least for simple ones like using an iterator variable directly instead of dereferencing it?
Those two are connected. Without strict typing the compiler doesn't know really whether the error is in the definition of min() or in its use, so the compiler mangles the two together and lets the programmer figure it out.
> In case of C++ templates, is it possible to output one error like: eg "class UserData needs to implement operator< to be used in min() function" instead of 200 lines errors?
This is already handled by concepts, but concepts solve only half the problem. E.g. in my snippet, in the comment you replied to, if any of the structs didn't have the kId constant, the errors would be about that. Concepts correctly detect when someone passes to them things that don't implement required functionality (as long as this functionality is specified in the concept). The other half of the problem, which they do not solve, is checking the correctness of the definition of the template against the concept. E.g. in my snippet I could use the "value" field, even though the concept that I used didn't declare it anywhere. At that point, if I passed a struct, which doesn't have that field, the compiler wouldn't be able to tell where the error is: in the template, or in its use --- so it would emit one error for each template instantiation, just as it did for "kIs" (because it can't know that it's just one typo in one place).
> Concepts correctly detect when someone passes to them things that don't implement required functionality
Unfortunately C++ Concepts check syntax even though as you'd expect the intention of the feature is to reflect semantics there is deliberately no way to capture that.
So e.g. Rust has traits PartialEq and Eq reflecting things that claim to be partial or full equivalence relations. There's no syntactic difference, a Rust type can't syntactically be impossible to check for equivalence with some values and not others but e.g. Rust's f32 implements PartialEq not Eq due to NaNs whereas u16 has both since integers all exhibit equivalence.
But in C++ there's just std::equality_comparable and it matches both integers and floats even though NaNs aren't in fact comparable because the float type syntactically has the == operator.
So this means in C++ concepts matching means your program compiles but it might blow up at runtime thanks to a difference in functionality rather than syntax.
To C++ programmers this is positioned as yet further responsibility on their shoulders. They must check that the data they're using actually models the concept that was required, it's not anybody else's job and if they get it wrong the program blows up.
I mean, I feel this particular example is really more of standard library design wart that occurs with any kind of syntactic overloading mechanism, e.g. Haskell has the exact same problem as C++ does here even though the overloading mechanism is rather different (Eq is defined for Float and there is no notion of a PartialEq in the standard library, even though there really should be), and there are numerous more advanced examples of that (every few years someone writes a fun library that violates the Monad laws, for instance). The fundamental question is: does an instance of an overloading mechanism respect the "properties" you expect of it, algebraic ones or not? Your program will often behave weirdly if the answer is "no" but the extent of the weirdness permissible is a spectrum. Basically no languages actually allow you to validate those semantics; it's expected that the "validity" of those instances is proven or known a priori, and bad ones don't exist.
IMO, the bigger problem with this particular example is just that the numeric and algebraic hierarchies in most languages are littered with various edge cases just like this one, and that makes it an easy and convenient nitpick to point out. But I think the parent comment is largely correct in that the goal of a concept (or trait, or typeclass) is roughly to reflect a property or action of some object or type or what have you, even if C++ wasn't as originally as forward thinking as other languages today are with invariant safety.
The practical difference between C++ Concepts and Rust Traits is that Rust Traits get to have semantics by declaration and you can't do that with C++ Concepts.
A Rust type gets to choose whether to be Eq or just PartialEq. I can wrap floats up in a new type, MyFloat, provide an implementation of == to qualify it as PartialEq and then I can choose whether to just declare that it's Eq. The compiler cannot check I told the truth, but as implementer I can decide whether to promise that or not.
In C++ you do not get a choice. C++ concepts refer to syntax and only that. I can write a concept named hates_balloons and have it depend on types implementing a zero argument method named "pop". But if your stack class with a pop method doesn't in fact hate balloons too bad, my concept says that since the syntax matches you have no choice, your stack hates_balloons and there is nothing you can do about it.
I understand exactly why C++ did this. There is a lot of legacy C++ code out there. If you make every new C++ iterator say it implements the iterator concept and don't grandfather the existing ones, nobody uses the feature. But, even though they had a good reason to do this, it's still a considerable weakness.
Compiler encounters operator<(T) on argument (type T) is called.
In best case it sees it was a function parameter. In worst case it at least sees concrete type of the parameter. Let's say that type is UserData
It will say that UserData type needs to implement UserData::operator<(UserData) to be able to use min() function.
Of course I can see this getting complex as nesting etc.. increase. But I am wondering if clang (since it's selling points include error messages, though gcc is catching up), hasn't tried something like this, there must be a fundamental technical difficulty for generalizing such a technique.
I highly recommend the last episode of the “Corecursive” podcast. The guest of the episode is Zig creator - Andrew Kelly, and he tells about the history of Zig, it’s really interesting! https://corecursive.com/067-zig-with-andrew-kelley/
Yes, and in principle you can generalize the type language into a full language with lambdas and such (type constructor is a lambda(ground type)->ground type), though you typically want it to be strongly normalizing so you know that the compiler will eventually halt. Then you don't need kinds, kind polymorphism, sorts, etc., you just have a dependently typed language with types and universes.
121 comments
[ 4.6 ms ] story [ 198 ms ] threadMaybe we should think about this in terms of space occupied by the languages and not the language themselves. One of the features of C is that pretty much anything can interface with it. Another is that you can write very fast code with it. Another is that pretty much any platform has a C compiler. Another is that it's small and fits in your head. Depending on what you're trying to do, you might use one "C replacement" or another.
Another way of looking at it would be to see the shortcomings of C you're trying to overcome. I think this way of thinking would be more productive than just thinking about "replacing C", "replacing C++" or things like that.
It's perfectly fair to compare Zig to C in this case, as the project originated as a dialect of C before the author decided to do a complete clean break[4]. The primary frame of reference when designing the language was C and addressing its shortcomings, both language and tooling.
[0]: https://ziglang.org/learn/overview/#export-functions-variabl...
[1]: https://github.com/ziglang/zig-bootstrap
[2]: https://ziglang.org/download/0.8.0/release-notes.html#Suppor...
[3]: https://andrewkelley.me/post/zig-cc-powerful-drop-in-replace...
[4]: https://www.youtube.com/watch?v=Gv2I7qTux7g
This is why I think talking about what you're trying to achieve is more fruitful than just talking about the language you want to replace. For example, if you want to replace C code and you don't need all of the performance in the world, you couold simply switch to a garbage collected language. In that case, since speed wasn't the most important requirement, a language that isn't usually thought as a C replacement can replace C code.
If you want to make the point that Zig is "closer to C" compared to other language that wants to replace C, that's a fair claim to make.
> People will often choose Rust instead of Zig because Rust has stronger guarantees about memory safety, and these guarantees are more important for them than having a language that's small and fits in your head.
For some people, having a language that's small and fits in your head is not a requirement, not a priority, and sometimes not even important.
This is definitely the right approach in my opinion. Even for your garbage collection example on the one hand, in some cases your program is dominated by tricky reclamation problems and the "correct" Rust program is basically 90% garbage collector, so that an existing GC language might produce faster programs; but on the far opposite scale maybe your program has tricky non-memory resource problems and GC doesn't help at all, so the GC language has worse performance and yet it makes your problem harder to solve than in Rust anyway.
One of the nice things about Rust and about C++ RAII here is that you don't care whether the resources are memory. Database handles? Aerial drones? A tape robot? These are all just resources you can wrap in an object to manage them, in both languages. "I only have 15MB of RAM" and "I only have 4 fire-fighting robots" are the same kind of problem from this perspective.
> If you want to make the point that Zig is "closer to C" compared to other language that wants to replace C, that's a fair claim to make.
This is really important. Zig is very much inspired by C. Rust has the syntax of a semi-colon language like C, but its inspirations come from all over the shop. I came to Rust having already programmed not only C and Java (and a little C++) but also Python and ML (and a little Prolog) and so this was less disconcerting. But I can see if you've literally never encountered a functional language some of Rust is weird because even though it is spelled like semi-colon languages you know it behaves very differently.
I like Rust, much more than I liked Go or Python when I picked those up. It feels less clunky. An elegant weapon, for a more civilised age.
One of the things I like most about Rust is how aggressively normal it feels to me, and it's really hard for me to remember what it's like to encounter this stuff for the first time.
Unfortunately, I think the "replacing X language" mentality happens because it's the easiest point to start with. The field of programming and computers is so vast now that most (all?) people have a very narrow view and it's easy to assume it's universal. I've lost track of all the "things all programmers should read/know/do" articles.
I think another effect is that if you really like a language, promoting it is one of the best thing you can do to work with it. When you see the recent (2010 and later) new languages, they all have a story of addressing a particular pain point. Typescript is Javascript at scale, Kotlin is old Java on android, Go is web services, Rust is performance and safety. This is solid marketing, and is part of how these languages end up dominating their domains. On the other hand, a language that's "X but slighly better everywhere" won't really attract people, since there is already code and jobs in that language.
Currently in the Great War Of Replacing C And C++, there are lots of fighters. Some people promote Rust, some others Go, some others D, some others Zig, some others to just keep writing C and C++. Part of the battle is writing code, libraries. Part of the battle is making the language and tooling itself better. And part of the battle is making articles, comments, talking to people and promote your language.
This is how C put other of business other system languages, much better than it. When you get UNIX and have already paid for a compiler, hardly any department will allow to buy additional compilers.
And C++, people tend to forget it was also born on the same place as UNIX and C, thus it was quickly adopted by all OSes that were UNIX clones in some fashion, and then by C compiler vendors on other platforms.
And on cases like Java, Go, TypeScript or Kotlin, you need a godfather company to push it no matter what.
Want to develop for Apple platform?
Depending on which level of the stack, the painless option will be C++, Objective-C or Swift.
On Android it will be a mix of Java (slowly being left behind on purpose), Kotlin or C++.
On Windows, .NET (and in some cases C# or C++/CLI only due to the way MSIL gets exposed) and C++. You can also keep using C if your view of Windows is like Windows XP APIs.
On Fuchsia, it is all about Flutter, C++, Go and Rust.
On ARM Mbed and Arduino, C++.
On microEJ, Java and C.
On Meadow, .NET.Core.
Azure Sphere, C.
CUDA, C++, Fortran, Python JIT and PTX aware backends.
There are plenty of other platforms I could keep on listing.
Naturally you can try to fit something else, but then it is on you and others to build the ecosystem, work around support issues and lack of IDE tooling.
There is a budget to be fulfilled for project delivery acceptance, going beyond that is wasted money.
I can hardly think of any language addon more innocent than Cpp (the preprocessor).
Imagine what madness would have emerged if it had proper variable assignment, goto:s or any loop construct?
I know this is a common view, but I want to highlight some counterexamples:
- C programmers need to learn about many different forms of UB: https://blog.regehr.org/archives/1520. Most take years to learn these rules. Many never learn them.
- Most real world C programs use some nonstandard language extensions on some target platforms. C learners trying to read production code have to navigate these.
- The C compilation and linking model is complicated and platform dependent. Compilation is a major stumbling block for students who have to learn Make or CMake at the same time they learn C.
There is some truth to the "C is simple" idea, I'll grant that. It's obviously simpler than C++. But I think there's a big effect where we tend to forget how hard it was to learn, because we learned it so long ago. There's also been substantial progress since the 70's on designing simpler programming languages.
Perl was my main language for over a decade, and when I wanted to go lower, I honestly struggled... but not with the language itself, with the peripherals which are exactly what you described!
> C programmers need to learn about many different forms of UB: https://blog.regehr.org/archives/1520. Most take years to learn these rules. Many never learn them.
UB really don't care that much in real life. When you are targeting a particular platform, that for most C programmer is some sort of 8/32 bit microcontroller, the only thing you care is the datasheet of the microcontroller. Of course you do things that in general are undefined behavior, but in the particular case lead to a perfectly predictable result.
In reality you only have to care about UB if your goal is to share code between hardware platform. But let's be realistic, most of the time you don't. Even if you do, some UB are these days standardized, for example integer overflow is UB, in practice you know that on every architecture 2 complement arithmetic is used and thus is well defined.
The real problem is that compiler for a misinterpretation for the standard started treating UB as "you are allowed to do whatever stupid thing you want since the condition will never happen", that is the real problem. In reality if you compile without optimization and with flags that avoid this stupid decisions it's not a problem.
> Most real world C programs use some nonstandard language extensions on some target platforms. C learners trying to read production code have to navigate these.
Again, it's a problem only if you are targeting different hardware platforms. But if you only program on ST microcontrollers (for example) why should you care?
And you must use them in some situations, for example in C there is not a way to control how a struct is packed (to decode efficiently data from a serial connection for example you define a packed structure and access individual fields). Or inline assembly is not in the standard (what if you need to use an instruction of the microcontroller that is not exposed by the C library?).
> The C compilation and linking model is complicated and platform dependent. Compilation is a major stumbling block for students who have to learn Make or CMake at the same time they learn C.
It's. But most of the time you stuck on whatever the development environment for the target hardware platform is setup to. Most C programmers doesn't even know what CMake or Make is, I care about it and do things by hand, most other people use IAR or Keil or whatever proprietary software that integrates compiler, build system and IDE.
Worse programs. Written by people who believe "C is not a difficult language" and who sacrificed all benefits of using a high performance language because they couldn't understand what they were doing.
So long as you don't have any competitors who use a better language or skilled programmers you can coast along like this, putting out a sub-par product and just pretending it's the best possible. But you can get a very sudden and rude awakening if anybody decide to offer what's possible here.
Here is a recent project that uses nim for AVR platforms, for example: https://github.com/PMunch/badger
I’ve seen the “clean up errors yourself” argument before, but, like the author, I don’t think it holds water. Often the correct response to errors is to panic() or pass it up the stack so the caller can deal with it or—more likely—panic() itself.
You get compile errors if you try to handle an impossible error, or don't include an `else` prong (in which case the compiler tells you the full set of unhandled errors).
Unfortunately most common languages does not prioritize this. As a contractor I have dug myself thru a lot of awful code in different languages and the most common denominator is improper error handling.
>He posted a follow up about error handling......
When I was reading the original blog I was thinking if he had any follow up, so I decide to click on archive and the homepage. And this article, somehow doesn't show up in both list. As a matter of fact I couldn't even find how to get to this blog post without your direct linking. Then it turns out it is a "FUTURE POSTS".
How does that work and why? Is this suppose to be some sort of preview before it is officially published?
The author posted the link in the comments of the original post.
That is a great idea. Thank You I might start doing that as well.
Handling Errors causes us developers so confusion, because we are so used to exceptions and how poorly they were conceived.
Errors should be called Failures. It's just when a function fails to so what it says it does. Nothing more, nothing special about it!
If you call fopen() and it doesn't find the file, that's a Failure! No reason to panic! Just go create the file!
The Zig equivalent seems like it would be roughly (I don't know Zig)
which is quite a bit more palatable and future-mistake-resistant. A sizable chunk of that is due to `try` of course, but still.Especially for tracing/logging of errors returned from functions; or for e.g. rolling back DB transactions in the event of an error.
Here's some ~100 examples from my work's (Sourcegraph's) codebase[0]
[0] https://sourcegraph.com/search?q=context:global+repo:%5Egith...
On Error Goto line
Looked this up and line can also be a label. Great.
(Also I'm not sure if the compiler would catch this, but there seems to be a bug in that Go code that's hard to see at a glance. You're never assigning a value to result)
Which can be worth it in a large enough project - `func()` and `func() error` will take care of like 90% of the signatures you'd defer anyway. I'd probably do that personally, unless I only needed it once or twice.
There has been a somewhat similar check/handle proposal (close to "try"), but it has consistently fallen into "they're great for trivial cases, but they make it harder to have good error messages / wrap errors with additional context", which is absolutely necessary in Go since errors are so loosely typed and contain only explicitly-added information (no stacks by default, for example). So I'm glad they haven't happened yet.
That said, if you're putting your language up as a "see the neat thing I did that's helpful in this very specific domain", and not general purpose, with the intent of supporting, it's probably immaterial to you.
Although these days people want to apply their favorite language to everything, I don't think zig will be a main language for writing web backends or http services. it's place is game engines, computer graphics, kernels, drivers etc..
A server isn't necessary, but 100% yes for a client. There's a lot of eating your own dog food needed for an http client, like string handling with encodings, network bindings, various collection types, async I/O and/or binary streams, error handling. The resulting client showcases the strengths of the language and its standard library, and most data is on the web today.
And yet that one guy still managed to build in useful features to his language that the Google "geniuses" couldn't think of despite (or because?) decades of experience.
If he was looking for a better C, Zig definitely fits the bill.
The other domains where C++ was strong for general purpose applications have been taken over by managed compiled languages like Java and .NET, which anyway make use of C++ libraries alongside their AOT/JIT compiled stacks.
There is little value to add from Rust in such scenarios, e.g. it won't make my WinUI application, CUDA shader or weather simulation model, any more safer than they already are.
CUDA supports C++ since version 3.0, and NVidia designs the cards to follow C++ semantics, regardless of the polyglot capabilities from PTX.
Hardly an afterthought, it was Khronos that became surprised that most researchers don't want to use plain old C on their research efforts.
"CppCon 2016: “Bringing Clang and C++ to GPUs: An Open-Source, CUDA-Compatible GPU C++ Compiler"
https://www.youtube.com/watch?v=KHa-OSrZPGo
"CppCon 2019: Olivier Giroux “The One-Decade Task: Putting std::atomic in CUDA.”"
https://www.youtube.com/watch?v=VogqOscJYvk
"Octane render. The Future of GPU Rendering: Real-Time Raytracing" (with CUDA)
https://www.youtube.com/watch?v=wGTIGjgXs6M
NVIDIA HPC SDK
https://developer.nvidia.com/hpc-sdk
It also aims to displace C++ when that's used in the same space, but in the sense that that's ground C++ gained from C, well C is the real target, if it can displace C there it can quite naturally displace C++ as well.
When I program on my own I often write an idea in C and then convert it to C++ when complexity rises, precisely because C++, in terms of syntax, is an incremental update to C. Using say Rust would entail a more serious rewrite.
Unfortunately Zig and Rust are quite different in syntax. I would love to change my workflow from C->C++ to Zig->Rust but the transition is not as nice.
- You can get cross-compilation for free in Cython projects just by switching compilers.
- (nearly) Arbitrary width integer types are a godsend for the right kinds of bit twiddly projects.
- The error handling is actually really nice.
- @ everywhere, for that I would keep using Objective-C
- Compiled languages shouldn't be using text file imports as modules
- I already have C and C++ for use after free/move errors
Free to disagree, it just means I am not part of the target audience.
If you are triggered by the AT character, I recommend that you stay away from Slack and Microsoft Teams.
Zig doesn't use C-style textual includes, if that's what you mean.
I expect a proper binary module, with native code and symbol table.
@import also has a second mode where the string isn't a Zig source file path, but a package name. AFAIK currently this also just resolves to a package-root zig file, but it could just as well be extended to some sort of binary format, the question is just: why?
If I want to distribute source code I would be using scripting languages.
It is also a nagging point for me when I play around with cargo, which contrary to conan and vcpkg, doesn't do binary libraries.
By the way, C++ modules might get a binary format definition by C++26 (too little time to sort it out by C++23), based on BMI/IFC formats already being deployed in GCC and VC++ respectively.
`pub extern fn foo(c_int) c_int;` etc.
then import with `usingnamespace @import("./my_header.zig");` and now you have a function `foo` that you can call; then you just need to link with the object file that defines it. Is your complaint that you still need the "header" file? What language / tools do you use that let you skip that part?
https://scattered-thoughts.net/writing/how-safe-is-zig/
Haskell solves that with typeclasses, Rust solves that with traits. What's really important about these solutions is that when you write a function generic over a trait or a typeclass, you are limited to using what is defined in the trait/typeclass. The trait/typeclass function both as documentation and as something that gets you better compilation errors.
"typename" in C++ is basically "any", but for values of template execution. Typeclasses/traits on the other hand are a sort of types for those values (which we normally call "types"). And similarly, in Zig there is an "any" type for types, just like in C++ --- it's called "type", and AFAIK there is no way to constrain your types with types of types.
I have a question to the compiler / IDE people here. In case of C++ templates, is it possible to output one error like: eg "class UserData needs to implement operator< to be used in min() function" instead of 200 lines errors? I mean, complex cases it's not possible but at least for simple ones like using an iterator variable directly instead of dereferencing it?
> In case of C++ templates, is it possible to output one error like: eg "class UserData needs to implement operator< to be used in min() function" instead of 200 lines errors?
This is already handled by concepts, but concepts solve only half the problem. E.g. in my snippet, in the comment you replied to, if any of the structs didn't have the kId constant, the errors would be about that. Concepts correctly detect when someone passes to them things that don't implement required functionality (as long as this functionality is specified in the concept). The other half of the problem, which they do not solve, is checking the correctness of the definition of the template against the concept. E.g. in my snippet I could use the "value" field, even though the concept that I used didn't declare it anywhere. At that point, if I passed a struct, which doesn't have that field, the compiler wouldn't be able to tell where the error is: in the template, or in its use --- so it would emit one error for each template instantiation, just as it did for "kIs" (because it can't know that it's just one typo in one place).
Unfortunately C++ Concepts check syntax even though as you'd expect the intention of the feature is to reflect semantics there is deliberately no way to capture that.
So e.g. Rust has traits PartialEq and Eq reflecting things that claim to be partial or full equivalence relations. There's no syntactic difference, a Rust type can't syntactically be impossible to check for equivalence with some values and not others but e.g. Rust's f32 implements PartialEq not Eq due to NaNs whereas u16 has both since integers all exhibit equivalence.
But in C++ there's just std::equality_comparable and it matches both integers and floats even though NaNs aren't in fact comparable because the float type syntactically has the == operator.
So this means in C++ concepts matching means your program compiles but it might blow up at runtime thanks to a difference in functionality rather than syntax.
To C++ programmers this is positioned as yet further responsibility on their shoulders. They must check that the data they're using actually models the concept that was required, it's not anybody else's job and if they get it wrong the program blows up.
IMO, the bigger problem with this particular example is just that the numeric and algebraic hierarchies in most languages are littered with various edge cases just like this one, and that makes it an easy and convenient nitpick to point out. But I think the parent comment is largely correct in that the goal of a concept (or trait, or typeclass) is roughly to reflect a property or action of some object or type or what have you, even if C++ wasn't as originally as forward thinking as other languages today are with invariant safety.
A Rust type gets to choose whether to be Eq or just PartialEq. I can wrap floats up in a new type, MyFloat, provide an implementation of == to qualify it as PartialEq and then I can choose whether to just declare that it's Eq. The compiler cannot check I told the truth, but as implementer I can decide whether to promise that or not.
In C++ you do not get a choice. C++ concepts refer to syntax and only that. I can write a concept named hates_balloons and have it depend on types implementing a zero argument method named "pop". But if your stack class with a pop method doesn't in fact hate balloons too bad, my concept says that since the syntax matches you have no choice, your stack hates_balloons and there is nothing you can do about it.
I understand exactly why C++ did this. There is a lot of legacy C++ code out there. If you make every new C++ iterator say it implements the iterator concept and don't grandfather the existing ones, nobody uses the feature. But, even though they had a good reason to do this, it's still a considerable weakness.
I was thinking something like this:
Compiler encounters operator<(T) on argument (type T) is called.
In best case it sees it was a function parameter. In worst case it at least sees concrete type of the parameter. Let's say that type is UserData
It will say that UserData type needs to implement UserData::operator<(UserData) to be able to use min() function.
Of course I can see this getting complex as nesting etc.. increase. But I am wondering if clang (since it's selling points include error messages, though gcc is catching up), hasn't tried something like this, there must be a fundamental technical difficulty for generalizing such a technique.
It is interesting that it bears some surface level similarities to type constructors in languages with higher kinded types.