Thanks for the write up. It can always be good to get a critical look in the mirror.
Having read your article, I think it comes down to some fundamental differences in views on programming. I donāt mean that in a bad way. I mean it in a way that we just have different values and goals when it comes to programming that influences what languages we like to work in.
As an example letās take a look at the no destructors part of the article. I personally like that Zig does not have destructors. The defer patern works well and I know exactly what code is going to be run. Part of simplicity is not hiding complexity. I like knowing what my code is doing and not having implicit function calls that hide the complexity of what Iām doing from me.
When it comes down we might just disagree, and Iām glad you are enjoying Rust.
I would have loved something like a # linear marker that would cause a linear resource to be dangling if itās not eventually consumed by a function taking it with the marker as argument
Has this been discussed before? Something like a type qualifier (similar to const) that causes an error when you leave the scope of a variable without returning/passing it somewhere. Without a complex type/borrowing system it couldnāt be completely correct in every case, but would still be helpful to prevent accidentally forgetting to deinit things, even if you have to manually cast it away when necessary.
FWIW, in my experience, literally all Zig code that uses try
/defer
/errdefer
a bunch is buggy. Like, after 3 minute of search, thereās an fd leak on error here:
ah, thereās an fd leak even if thereās no error.
Browsing quickly through the text, I mostly agree with the good and less good things of Zig.
What I like most is the readability (except the dreaded āanytypeā). I moved from Rust to Zig, because I think Rust is (1) unreadable and (2) has a borrowchecker which I am completely not interested in. (I like the freedom Zig gives to do anything).
Welcome to Ziggit! It sounds like you have more low-level experience than I do, but I am curious to hear some expansion on some of your points.
Specifically, contracts and comptime. Personally, one of the reasons I stopped learning rust was because I didnāt want to have to learn how to write macros- adding AST traversal to an already complex language was a bridge too far for me.
While Iām not yet 100% convinced that comptime can fully replace the functionality of macros, it does seem to match the 80/20 rule for me, and possibly more.
Similarly, I do see the value in traits, but I think a paradigm shift could get me to live without them.
AFAICT, you can write programming contacts in Zig using comptime to build interfaces. You canāt do it as easily as you can with traits, and you canāt share partial contacts in the same manner, i agree that it can be irritating to dig around in abstracted code and find what youāre looking for but ultimately is it that you canāt build contracts, or is it that itās harder to share code between contracts?
Sorry if my tone is aggressive, like I said I have less experience, and im just trying to wrap my head around the issues.
That was my experience my first few years with Zig as well. Iāve become better over time (as youād expect) by obsessively following the same pattern everywhere, but asking fallible humans to manage resources as well as a machine is reminiscent of the old folk tale John Henry and his battle against a steeldriving machine. He won the battle by a single nail but died of exhaustion afterwardsā¦not sure what the lesson there is for Zig programmers :^D
Iām not willing to give up simplicity (and explicitness) by introducing hidden function calls like destructors, but nobody should be under the illusion that this is a solved problem. The author mentioned linear types as a possible alternative and Iām holding out hope for that, but it seems like too big of a change to ever introduce to Zig.
In the meantime there are a couple smaller things we could do:
- Throw an error on fd leaks in debug mode, just as we do with memory leaks.
- Find a better way to test error paths. Most of my resource management mistakes are in error paths because my unit tests donāt trigger them enough.
Same here. My pattern is that I am terrified of resource management in Zig, and try to go out of my way to not have to manage resources. It is a good pressure to design software better, but its a bit like learning to swim in a sea full of sharks
I originally came across this article when ThePrimeagen reacted to it on stream. The main take away I had from it was the closing remark. Specifically, this one:
Iām also fed up of the skill issue culture. If Zig requires programmers to be flawless, well, Iām probably not a good fit for the role.
Perhaps my own bias is showing here, but this has never been my experience from this community (or any Zig community). I suspect there was a human element to writing this critique. While some of his points of criticism are valid (e.g. Rust Result
s are really convenient for propagating additional info in error paths), others feel invalid and quite subjective.
The most egregious example is the authors section Memory safety is highly underestimated and fallacious. I have read @andrewrkās blog the author references, but I feel that this writing was really spun into a topic much bigger than Andrew was intending. I donāt know any reasonable Zig enthusiast that would sincerely argue that Zig is safer than Rust. Instead, those enthusiasts would argue trade-offs and say what Zig looses in safety, it makes up with < INSERT FAVORITE FEATURE >.
This section of the blog read to me as a Straw man argument from an author deeply entrenched in ideological tribalism.
But I want to be self-reflective and noticed my own bias. Perhaps Iām ideologically entrenched in the tribe of Zig and itās blinding me from seeing the critique in a neutral light. What do you all think?
I would also add here the principle of locality that helps to tackle complexity. In Zig you are less likely need to jump around the codebase to understand what a given block of code is doing. With destructors (hi C++) and traits (hello Rust) one has to be all over the place to mentally unwind the hidden logic.
Thatās fair. Defer is easy to miss, even for experienced devs (not that Iām one), but I like that it pushes you to think about things as bigger chunks, rather than individual items that need to be built and un-built one at a time.
I understand this sentiment exactly, and your thoughts/analysis were near identical to my own, though you managed to articulate them better than I could have.
I do not for a moment believe that the author wrote the article in bad-faith, quite the opposite, they seemed to truly approach the subject in the most neutral way they could. That said, the impression that I got from their conclusion and many of their criticisms (a few of which are valid) could be reduced to down to āZig is not Rustā, which they view as a negative against the language.
As you stated, I am unsure if this is their bias for Rust, my own for Zig, or a mixture of both, but it was the impression that I walked away with after reading the article. I understand the perspective of the author was one coming from the Rust world, but in my opinion, the entire article could be summarized with the following bullet-points:
- Pros: Zig is like Rust in these ways. It also has arbitrary-sized numbers.
- Cons: Zig is not like Rust in these ways
Iāve gotten in the habit of doing focused passes looking for this sort of bug, and yes, I do commonly find them. It might take you more than three minutes (hey it might not) to find another one in my public code, but I do very much doubt that itās all properly picked over.
That said, if Iāve gone over a function specifically linting for those problems, itās generally solid. There are some blunt instruments available like errdefer comptime unreachable
to leave tripwires for checking again after further modifications, but generally speaking any change to a function / file means itās a ādirty pageā and it will need to be gone over again. It doesnāt work to try and do this while developing, itās a decided shift in mindset.
As it happens I donāt think this is acceptable, we shouldnāt just shrug, tell everyone to git gud, and leave it at that. Itās a growing preoccupation of mine that Zig needs a validation system: some way to annotate intention in the code, hopefully better than magic comments, and an open-ended compiler hook to run validations in an attempt to prove, or disprove, those statements.
What Iām not interested in is any attempt to add resource management to the type system. Rust is right there, showing what that looks like, advantages and disadvantages both. Occasionally someone who has the Rust missionary zeal shows up (hi @Md5) and tells us why Zig should be Rust, and I always wonder: why? Whatās wrong with Rust that Zig should become it? Would it end up being a better Rust? Great, do that to Rust, skip the middleman.
The memory policy enforced by affine types has known advantages, but, it covers a tiny sliver of design space, and any honest Rust enjoyer will admit that theyāve had to make substantial changes to their design plans to satisfy the type system. Generally this comes with bold assertions that the original idea was a mistake, how much of that is accurate and how much Stockholm syndrome I do not know.
Itās not resource management which is hard, itās defect-free programming. High assurance software is hard! I hope Zig will continue to make progress on the entire problem, rather than nerfing the language by imposing an acceptable but highly opinionated and brittle solution on one part of it.
Related, one thing I realized recently is that IO-as-an-interface is also the obvious place to put resource leak checks. For example just like we have leak checking in DebugAllocator we can have file descriptor leak checking in Io implementations.
Itās also potentially an area where we can have something like checkAllAllocationFailures
but for testing multiple kinds of scheduling orderings. Unit tests can be repeated with different, deterministic scheduling orderings.
Also fault injection, making the Nth I/O operation fail with some kind of error.
I think when we have all these things, combined with the integrated fuzzer working well, itās going to be a new era of Zig.
Great write-up always interesting to hear from someone using Rust. The only point Iād push back on is the idea that simplicity isnāt a feature.
Zig is basically C with modern conveniences. Like C, youāre playing in manual mode you have to be careful, stay mindful, and keep things simple. Itās not for everyone but at least itās real. Thatās what pushed me away from C++ and Rust. They pretend to be simple, but they are very complex machinery.
In C++, Iāve had too many days where I felt productive, feeling like I was getting shit done, only to realize I spent the whole time wrangling headers and boilerplate. Iād feel good because the code didnāt crash immediately until I turned on UBSan and saw the mess.
Worse, the knowledge I gained in those languages didnāt translate over others. It was all C++ specific. With Zig or C, thereās less to learn, and what you do learn actually carries over. You donāt waste time debating which of 20 ways to solve a problem is best. You just write the code and move on.
This Iām glad to hear. Iāve tried to figure out ways to make a ātestAllErrorPathwaysā-type system (generalizing testAllAllocationFailures, basically) work using just comptime, and I donāt think itās possible, because declarations and functions live outside of the type reification system. Barring code generation it would take compiler support to get it.
The allocation failures version works because Allocator is an abstract interface, so it makes sense that doing that for I/O would make it possible there as well.
I have to concur about the shadowing for exactly the same reasoning.
I disagree about strings simply because everybody disagrees how strings should be implemented in every language. Everybody wants āsomebodyā to implement a string, but there is no convergence as to what features āstringā should implement and what guarantees should be provided. Because of that, strings donāt belong in the language; strings belong in a library.
ādeferā is okay, but it is definitely error prone and limited (lexical scoping can often get in the way of your ability to run the defer in the correct spotāespecially for errdefer). Iām increasingly of the opinion that destructors/finalizers randomize performance sufficiently on modern processors that you should acknowledge that needing them is badly done garbage collection and you really should be using a language with GC, instead. This is also reinforced by the fact that languages with destructors/finalizers always gain a body of complex idioms for how to program in such a way to sidestep them/make them deterministic.
Instead of destructors, what Iām wondering if Iād like is an explicit annotation bit on a variable with runtime checks that say āHey, if this variable drops out of scope with this bit still set, shout.ā That way it would be up to the programmer to opt-in to the check by tying āresource->variableā and be forced to unset the bit to indicate that the resource has been handled or acknowledged by passing it out.
Iād like to expand on this point by saying that simplicity isnāt just a feature, itās an incredibly difficult and hard fought one- particularly in a project as ambitious as Zig is.
When people propose minor additions to the language and get immediately shot down, Iām not sure they realize the long term impact that those additions can have on a language.
One day youāre accepting a minor convenience change, the next day another, and pretty soon you wake up to realize your language has turned into c++ (if I understand my history correctly).
I applaud the Zig team for sticking to their vision, and I do think that the language should be kept simple, as long as you arenāt bending over backwards to make things work.
Look at how long it took golang to accept generics.
First Iād like to thank @Md5 for sharing his findings, in particular Iād like to thank the sober tone he did it with. Itās clear from the effort he has put into his blog post that he wants nothing more than to be a zig fanboy, but alas it seems the distance from what zig is and what he wants is too far.
There are a few key take aways that I find ridiculously accurate, I simply love his # on return values suggestion that tells the caller and the compiler that the return value is responsible for owning resources and as such must be stored or properly disposed of by the user.
If you donāt take the # then please could we without calling it a destructor just say that every struct that doesnāt contain a deinit has a deinit no-op that makes defer something.deinit(); valid syntax even if it does absolutely nothing except making it a safe way to say Iām only using this value in this scope and I donāt care, just get rid of it. Of course there are the structs where this cannot work, say a struct that owns memory but doesnāt have a reference to the allocator, those cases can then add a comptime deinit with a compiler error. Complete with the proper steps to dispose of them. That would really cut my learning curve in half on most libraries.
I donāt particularly agree with many of his Rust points, last time I looked at Rust code my eyes hurt. He is right that it will protect you from memory errors, but at what a cost? That is just painful! Give me the debug allocator any day! Again I totally buys his premise that Rust works, itās just not for me.
In fact Iād say I love the solutions in the zig language. The whole deal about getting rid of async by simply allowing you to set up your io stack to do fibers behind your back⦠without changing a single line in your implementation, whatās not to love? That beats async any day of the week!
On the other side Iām very surprised when I look at the Writer structs and indeed the newly proposed IO stuff. The lengths you go to in order to avoid interfaces, classes and anything called OOP. And yet youāre making structs that takes code from other structs and uses VTables to glue it all together so that the caller can abstract away the difference. That at itās core is what OOP is doing. Yes most OOP languages does a lot more than that⦠a lot more I donāt want zig to add! But similar to allowing functions to be nested in the struct (the very first step of OOP) letās agree that if we need to make wrapper structs and with VTables (or interfaces as I like to call them) for the std library, then perhaps it would be nice to add some sort of language feature to facilitate this instead of forcing everyone to make this rather cumbersome exercise.
I hope that when @andrewrk chose the wording āIO-as-an-interfaceā it meant he is aware of this. The latest version of struct with VTable could be used to avoid adding it as a language feature, but why keep calling it a digging implement instead of a shovel? When deciding on what feature to add, I really hope they consider some sort of contract that will help people to avoid using anytype for stuff that clearly has a contract, a well implemented interface pattern would be my personal preference, but donāt let that limit you, same as with IO, take your time, let it simmer and then make an excavator instead of a shovel.
Yep jumping around the codebase is a recipe for logical flaws. In the end, zig is explicit and rust has lots of implicitness. I think adding safety to explicit is always possible but taking away implicitness is not always possible, so zig should not be trapped in a local medium.
IMO zig should not add linear types. As per my zig clr experiment, refinement types can be added to zig ~without changing the language (if you have access to AIR and you accept the use of a checker tool). Refinement types are a superset of linear types.
āHey, if this variable drops out of scope with this bit still set, shoutā
This is checkable in zig at compile time with refinement types.