Looking into Zig
ayende.com
ayende.com
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.
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...
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.
fn canError(foo: bool) !void {
if (foo) {
return error.Foo;
} else {
return error.Bar;
}
}
test "canError" {
canError(true) catch |e| switch (e) {
error.Foo => @panic("foo!"),
};
}
Running `zig test` on that file would give: ./tmp/errors.zig:10:30: error: error.Bar not handled in switch
canError(true) catch |e| switch (e) {
^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!
// 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.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.
try
...
except
end
wrapped around the error/exception generating code(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)
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.
- @ 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?
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.
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.
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.
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
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.
It is interesting that it bears some surface level similarities to type constructors in languages with higher kinded types.