11 ms·
Zig; what I think after months of using it
- taurknaut 2y agoI loved this deep-dive of zig. > There’s a catch, though. Unlike Rust, ErrorType is global to your whole program, and is nominally typed. What does "global to your whole program" mean? I'd expect types to be available to the whole compilation unit. I'm also weirded out by the fact that zig has a distinct error type. Why? Why not represent errors as normal records?
- naasking 2y agoI'm not speaking for Zig, but in principle errors are not values, and often have different control flow and sometimes even data flow constraints.
- valenterry 2y agoCan you elaborate more?
- chrisco255 2y agoThey're enums. See: https://zig.guide/language-basics/errors https://zig.guide/language-basics/errors
- valenterry 2y agoThere it says "errors are values" so now that contradicts what OP said.
- chrisco255 2y agoI was generally responding to the whole thread and pointing to how Zig sees errors. Enums are a type of value, yes, but they're typically dealt with differently than other data types.
- naasking 2y agoI said I wasn't speaking for Zig specifically, just on general principle that errors are not really values. Many languages reify errors as values to avoid having different semantics for errors, but errors probably should have their own semantics. Zig seems to take a middle ground here, where errors are a special type of value but that still sort of has its own semantics.
- valenterry 2y ago> I said I wasn't speaking for Zig specifically Lol, you are right. My brain just skipped that part somehow.
- lmm 2y ago> What does "global to your whole program" mean? I'd expect types to be available to the whole compilation unit. I think they mean you only have one global/shared ErrorType . You can't write the type of function that may yeet one particular, specific type of error but not any other types of error.
- chrisco255 2y agoThey're really just enum variants. You can easily capture the error and conditionally handle it: fn failFn() error{Oops}!i32 { try failingFunction(); return 12; } test "try" { const v = failFn() catch |err| { try expect(err == error.Oops); return; }; try expect(v == 12); // is never reached }
- lmm 2y ago> You can easily capture the error and conditionally handle it Sure. But the compiler won't help you check that your function only throws the errors that you think it does, or that your try block is handling all the errors that can be thrown inside it.
- jamii 2y ago> ...the compiler won't help you check that your function only throws the errors that you think it does, or that your try block is handling all the errors that can be thrown inside it. It will do both of those: const std = @import("std"); fn throws(i: usize) !void { return switch (i) { 0 => error.zero, 1 => error.one, else => error.many, }; } fn catches(i: usize) !void { throws(i) catch |err| { return switch (err) { error.one => error.uno, else => |other| other, }; }; } pub fn main() void { catches(std.os.argv.len) catch |err| { switch (err) { // Type error if you comment out any of these: // note: unhandled error value: 'error.zero' error.zero => std.debug.print("0\n", .{}), error.uno => std.debug.print("1\n", .{}), error.many => std.debug.print("2\n", .{}), // Type error if you uncomment this: // 'error.one' not a member of destination error set //error.one => std.debug.print("1\n", .{}), } }; } It wouldn't hurt to just read the docs before making confident claims.
- jamii 2y agoWhat they're trying to convey is that errors are structurally typed. If you declare: const MyError = error{Foo} in one library and: const TheirError = error{Foo} in another library, these types are considered equal. Unlike structs/unions/enums which are nominal in zig, like most languages. The reason for this, and the reason that errors are not regular records, is to allow type inference to union and subtract error types like in https://news.ycombinator.com/item?id=42943942 https://news.ycombinator.com/item?id=42943942. (They behave like ocamls polymorphic variants - https://ocaml.org/manual/5.3/polyvariant.html https://ocaml.org/manual/5.3/polyvariant.html) This largely avoids the problems described in https://sled.rs/errors.html#why-does-this-matter https://sled.rs/errors.html#why-does-this-matter. On the other hand zig errors can't have any associated value (https://github.com/ziglang/zig/issues/2647 https://github.com/ziglang/zig/issues/2647). I often find this requires me to store those values in some other big sum type somewhere which leads to all the same problems/boilerplate that the special error type should have saved me from.
- throwawaymaths 2y agoif you need values associated with your error you can stash them in an in-out parameter
- jamii 2y agoIf I have multiple errors then that in-out parameter has to be a union(enum). And then I'm back to creating dozens of slightly different unions for functions which return slightly different sets of errors. Which is the same problem I have in rust. All of the nice inference that zig does doesn't apply to my in-out parameter either. And the compiler won't check that every path that returns error.Foo always initializes error_info.Foo.
- throwawaymaths 2y agoi dont think you can make a union that has an error enum tag. I dont see why the in-out has to be dependent on the error.
- hansvm 2y ago> global to your whole program Zig automatically does what most languages call LTO, so "whole program" and "compilation unit" are effectively the same thing (these error indices don't propagate across, e.g., dynamically linked libraries). If you have a bunch of ZIg code calling other Zig code and using error types, they'll all resolve to the same global error type (and calling different code would likely result in a different global error type). > distinct error type, why? The langage is very against various kinds of hidden "magic." If you take for granted that (1) error paths should have language support for being easily written correctly, and (2) userspace shouldn't be able to do too many shenanigans with control flow, then a design that makes errors special is a reasonable result. It also adds some homogeneity to the code you read. I don't have to go read how _your_ `Result` type works just to use it correctly in an async context. The obvious downside is that your use case might not map well to the language's blessed error type. In that case, you just make a normal record type to carry the information you want.
- 3r7j6qzi9jvnve 2y ago(never used zig yet myself) For UB detection I've read zig had prime support for sanitizers, so you could run your tests with ubsan and catch UBs at this point... Assuming there are enough tests. As far as I'm concerned (doing half C / half rust) I'm still watching from the sidelines but I'll definitely give zig a try at some point. This article was insightful, thank you!
- lnenad 2y agoWhen did shadowing become a feature? I was under the impression it's an anti-pattern. As per the example in the article > const foo = Foo.init(); > const foo2 = try foo.addFeatureA(); > const foo3 = try foo.addFeatureB(); It's a non issue to name vars in a descriptive way referring to the features initial_foo for example and then foo_feature_a. Or name them based on what they don't have and then name it foo. In the example he provided for Rust, vars in different scopes isn't really an example of shadowing imho and is a different concept with different utility and safety. Replacing the value of one variable constantly throughout the code could lead to unpredictable bugs.
- saithound 2y agoShadowing always has been a feature, doubly so in languages which lack linear types. It is a promise to the reader (and compiler) that I will have no need of the old value again. Notice that applying the naming convention you suggest does nothing to prevent the bug in the code you quoted. It might be just as easy to write const initial_foo = Foo.init(); > const foo_feature_A = try initial_foo.addFeatureA(); > const foo_feature_B = try initial_foo.addFeatureB(); but it's also just as wrong. And even if you get it right, when the code changes later, somebody may add const foo_feature_Z = try foo_feature_V.addFeatureX();. Shadowing prevents this.
- nine_k 2y agoSaid promise should also be checked for sanity. E.g. for i in range(N) { for i in range(M) { # Typo; wanted j. # The compiler should complain. } }
- Maxatar 2y agoThe Rust compiler would complain in this case that the initial i variable is unused. Unused variables should be named with an underscore, _.
- dpc_01234 2y agoShadowing is a feature. It's very common that given value transforms its shape and previous versions become irrelevant. Keeping old versions under different names would be just confusing. With type system there is no room for accidental misuse. I write Rust professionally for > 2 years, and years before that I was using it my own projects. I don't think shadowing ever backfired on me, while being very ergonomic.
- scubbo 2y agoGreat write-up, thank you! I used Zig for (most of) Advent Of Code last year, and while I did get up-to-speed on it faster than I did with Rust the previous year, I think that was just Second (low-level) Language syndrome. Having experienced it, I'm glad that I did (learning how cumbersome memory management is makes me glad that every other language I've used abstracts it away!), but if I had to pick a single low-level language to focus on learning, I'd still pick Rust.
- toprerules 2y agoAs a systems programmer, Rust has won. It will take decades before there is substantial Rust replacing the absurd amounts of C that runs on any modern Unix system, but I do believe that our of all the replacements for C/C++, Rust has finally gained the traction most of them have lacked at the large companies that put resources behind these types of rewrites and exploratory projects. I do not think Zig will see wide adoption, but obviously if you enjoy writing it and can make a popular project, more power to you.
- chrisco255 2y agoRust has very real limitations and trade-offs. It compiles slow and the binaries are large. The compiler also makes performance sacrifices that makes it generally slower than C. I'm sure the language will continue to be successful, but it hasn't "won".
- pkulak 2y agoWhy do you say slower than C? I’ve never seen a reason to believe they’re anything but roughly equivalent.
- Defletter 2y agoThe last I heard, Rust had issues with freeing memory when it wouldn't need to, particularly with short-lived processes (like terminal programs) where the the Rust program would be freeing everything while the C version would just exit out and let the operating system do cleanup.
- cwood-sdf 2y agoIt seems like he wants zig to be more like rust. personally, i like that zig is so simple
- zamalek 2y agoThis is absolutely not what the article is about. A good majority of it is spent on the myth that Zig is safer than Rust, which has nothing to do with wishing Zig was more like Rust.
- chrisco255 2y agoIs there a myth that makes that claim? Virtually every take I've heard is that Zig is "safe enough" while giving developers more control over memory and actually, it's specifically better for cases where you must write unsafe code, as it's not possible to express all programs in safe Rust.
- bobbylarrybobby 2y agoIf you must write unsafe code, what's wrong with just dropping down to unsafe in Rust when you need to? You have all the power unsafe provides, and you have a smaller surface area to audit than if your entire codebase resides in one big unsafe block.
- chrisco255 2y agoUnsafe Rust is problematic: https://zackoverflow.dev/writing/unsafe-rust-vs-zig https://zackoverflow.dev/writing/unsafe-rust-vs-zig See also: https://github.com/roc-lang/roc/blob/main/www/content/faq.md#why-does-roc-use-both-rust-and-zig-rust-and-zig https://github.com/roc-lang/roc/blob/main/www/content/faq.md... Zig is not entirely unsafe. It provides quite a few compile time checks and primitives to catch memory leaks or prevent them altogether.
- oneshtein 2y agoYes, unsafe code is problematic in Rust, C, C++, etc. Is Zig different?
- ethin 2y agoNo idea how much the author is experienced at Zig, but my thoughts: > No typeclasses / traits This is purposeful. Zig is not trying to be some OOP/Haskell replacement. C doesn't have traits/typeclasses either. Zig prefers explicitness over implicit hacks, and typeclasses/traits are, internally, virtual classes with a vtable pointer. Zig just exposes this to you. > No encapsulation This appears to be more a documentation issue than anything else. Zig does have significant issues in that area, but this is to be expected in a language that hasn't even hit 1.0. > No destructors Uh... What? Zig does have destructors, in a way. It's called defer and errordefer. Again, it just makes you do it explicitly and doesn't hide it from you. > No (unicode) strings People seem to want features like this a lot -- some kind of string type. The problem is that there is no actual "string" type in a computer. It's just bytes. Furthermore, if you have a "Unicode string" type or just a "string" type, how do you define a character? Is it a single codepoint? Is it the number of codepoints that make up a character as per the Unicode standard (and if so, how would you even figure that out)? For example, take a multi-codepoint emoji. In pretty much every "Unicode string" library/language type I've seen, each individual codepoint is a "character". Which means that if you come across a multi-codepoint emoji, those "characters" will just be the individual codepoints that comprise the emoji, not the emoji as a whole. Zig avoids this problem by just... Not having a string type, because we don't live in the age of ASCII anymore, we live in a Unicode world. And Unicode is unsurprisingly extremely complicated. The author tries to argue that just iterating over byes leads to data corruption and such, but I would argue that having a Unicode string type, separate from all other types, designed to iterate over some nebulous "character" type, would just introduce all kinds of other problems that, I think, many would agree should NOT be the responsibility of the language. I've heard this criticism from many others who are new to zig, and although I understand the reasoning behind it, the reasoning behind just avoiding the problem entirely is also very sensible in my mind. Primarily because if Zig did have a full Unicode string and some "character" type, now it'd be on the standard library devs to not only define what a "character" is, and then we risk having something like the C++ Unicode situation where you have a char32_t type, but the standard library isn't equipped to handle that type, and then you run into "Oh this encoding is broken" and on and on and on it goes.
- llimllib 2y ago> In pretty much every "Unicode string" library/language type I've seen, each individual codepoint is a "character" languages are actually really inconsistent on what they count as a unicode character: https://hsivonen.fi/string-length/ https://hsivonen.fi/string-length/ (I don't broadly disagree with you on unicode support, just linking an article relevant to that claim)
- edflsafoiewq 2y agoThe debate between static and dynamic typing continues unceasingly. Even when the runtime values are statically typed, it's merely reprised at the type level.
- smt88 2y agoThe debate seems to have mostly ended in a victory for static types. The largest languages other than Python have them (if you include the transition from JS to TS). Python is slowly moving toward having them too.
- Turskarama 2y agoI honestly don't see how anyone who has used a language with both unions and interfaces could come up with anything else that makes dynamic types better. Either way you need to fulfill the contract, but I'd much prefer to find out I failed to do that at compile time.
- ridiculous_fish 2y agoDon't confuse "presence of dynamic types" with "absence of static types." Think about the web, which is full of dynamicism: install this polyfill if needed, call this function if it exists, all sorts of progressive enhancement. Dynamic types are what make those possible.
- Turskarama 2y agoSure, I'm primarily a C# programmer which does have a dynamic type object, and occasionally use VB which uses late binding and can use dynamic typing as well. You want to know how often I find dynamic typing the correct tool for the job? It's literally never. Dynamic typing does allow you to do things faster as long as you can keep the whole type system in your head, which is why JavaScript was designed the way it was. That doesn't mean it is necessary to do any of those things, or is even the best way to do it.
- 2y ago
- SPBS 2y agoHeaders are missing IDs for URL fragments to jump to e.g. https://strongly-typed-thoughts.net/blog/zig-2025#error-handling https://strongly-typed-thoughts.net/blog/zig-2025#error-hand... doesn't work
- hadronized 2y agoI noticed that and fixed it on my lunch break. Sorry for the inconvenience!
- sedatk 2y ago> The first one that comes to mind is its arbitrary-sized integers. That sounds weird at first, but yes, you can have the regular u8, u16, u32 etc., but also u3. At first it might sound like dark magic, but it makes sense with a good example that is actually a defect in Rust to me. You don't need Rust to support that because it can be implemented externally. For example, crates like "bitbybit" and "arbitrary-int" provide that functionality, and more: https://docs.rs/crate/arbitrary-int/ https://docs.rs/crate/arbitrary-int/ https://docs.rs/crate/bitbybit/ https://docs.rs/crate/bitbybit/
- pcwalton 2y agoI'm normally not sympathetic to the "you don't need that" argument, but there is a much stronger argument for not having arbitrarily-sized integers in Rust: the fact that values of such types can't have an address. The reason why our types all have bit sizes measured in octets is that a byte is the minimum granularity for a pointer.
- chrisco255 2y agoA byte isn't the minimum granularity for a pointer. The minimum is based on whatever target you're compiling for. If it's a 32-bit target platform, then the minimum granularity is 4 bytes. Why should pointer size determine value size though? It's super fast to shift bits around, too, when needed.
- pcwalton 2y ago> If it's a 32-bit target platform, then the minimum granularity is 4 bytes. Huh? How do you think `const char *s = "Hello"; const char *t = &s[1];` works? > Why should pointer size determine value size though? Because you should be able to take the address of any value, and addresses have byte granularity.
- Gibbon1 2y agoWith a 64 bit pointer you could make it bit addressable.
- hoelle 2y ago> Zig does enhance on C, there is no doubt. I would rather write Zig than C. The design is better, more modern, and the language is safer. But why stop half way? Why fix some problems and ignore the most damaging ones? I was disappointed when Rust went 1.0. It appeared to be on a good track to dethroning C++ in the domain I work in (video games)... but they locked it a while before figuring out the ergonomics to make it workable for larger teams. Any language that imbues the entire set of special characters (!#*&<>[]{}(); ...etc) with mystical semantic context is, imo, more interested in making its arcane practitioners feel smart rather than getting good work done. > I don’t think that simplicity is a good vector of reliable software. No, but simplicity is often a property of readable, team-scalable, popular, and productive programming languages. C, Python, Go, JavaScript... Solving for reliability is ultimately up to your top engineers. Rust certainly keeps the barbarians from making a mess in your ivory tower. Because you're paralyzing anyone less technical by choosing it. > I think my adventure with Zig stops here. This article is a great critique. I share some concerns about the BDFL's attitudes about input. I remain optimistic that Zig is a long way from 1.0 and am hoping that when Andrew accomplishes his shorter-term goals, maybe he'll have more brain space for addressing some feedback constructively.
- pcwalton 2y ago> It appeared to be on a good track to dethroning C++ in the domain I work in (video games)... but they locked it a while before figuring out the ergonomics to make it workable for larger teams. There are million-line Rust projects now. Rust is obviously workable for larger teams. > Any language that imbues the entire set of special characters (!#*&<>[]{}(); ...etc) with mystical semantic context is, imo, more interested in making its arcane practitioners feel smart rather than getting good work done. C uses every one of those symbols. I think you're talking about @ and ~ boxes. As I recall, those were removed the same year the iPad and Instagram debuted.
- hoelle 2y ago> I think you're talking about @ and ~ boxes. As I recall, those were removed the same year the iPad and Instagram debuted. Take criticism better. A language choice on a project means the veterans are indefinitely charged with teaching it to newbies. For all Rust's perks, I judge that it would be a time suck for this reason. Browsing some random rust game code: [https://github.com/bevyengine/bevy/blob/8c7f1b34d3fa52c007b2bd3585446ec1b0234a65/crates/bevy_animation/src/transition.rs#L77 https://github.com/bevyengine/bevy/blob/8c7f1b34d3fa52c007b2...] pub fn play<'p>( &mut self, player: &'p mut AnimationPlayer, new_animation: AnimationNodeIndex, transition_duration: Duration, ) -> &'p mut ActiveAnimation { [https://github.com/bevyengine/bevy/blob/8c7f1b34d3fa52c007b2bd3585446ec1b0234a65/crates/bevy_input/src/button_input.rs#L123 https://github.com/bevyengine/bevy/blob/8c7f1b34d3fa52c007b2...] #[derive(Debug, Clone, Resource)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Default, Resource))] pub struct ButtonInput<T: Copy + Eq + Hash + Send + Sync + 'static> { /// A collection of every button that is currently being pressed. pressed: HashSet<T>, ... Cool. Too many symbols.
- grayhatter 2y agolol, I knew exactly who wrote this once I saw the complaint about shadowing being forbidden. The author and I were just arguing about it the other day on irc. While the author considers it an annoying language bug because it requires creating additional variable names (given refactoring was an unpalatable option). I consider it a feature. Said arguments have become a recurring and frustrating refrain; when rust imposes some limit or restriction on how code is written, it's a good thing. But if Zig does, it's a problem? The remainder of the points are quite hollow, far be it from me to complain when someone starts with a conclusion and works their way backwards into an argument... but here I'd have hoped for more content. The duck typing argument is based on minimal, or missing documentation, or the doc generator losing parts of the docs. And "comptime is probably not as interesting as it looks" the fact he calls it probably uninteresting highlights the lack of critical examination put here. comptime is an amazing feature, and enables a lot of impressive idioms that I enjoy writing. > 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. But hey, my joke was featured as the closing thought! Zig doesn't require one to be flawless. But it' also doesn't try to limit you, or box you into a narrow set of allowed operations. There is the risk that you write code that will crash. But having seen more code with unwrap() or expect() than without, I don't think that's the bar. The difference being I personally enjoy writing Zig code because zig tries to help you write code instead of preventing you from writing code. With that does come the need to learn and understand how the code works. Everything is a learnable skill; and I disagree with the author it's too hard to learn. I don't even think it's too hard for him, he's just appears unwilling.... and well he already made up his mind about which language is his favorite.
- YuukiRey 2y agoThe duck typing argument is absolutely not based on minimal or missing documentation. There wouldn't be countless issues about it in the Zig repository if it were that simple. See https://github.com/ziglang/zig/issues/17198 https://github.com/ziglang/zig/issues/17198 I'm simply going to quote one of the comments from the linked GitHub issue: > generic code is hard. Hard to implement correctly, hard to test, hard to use, hard to reason about. But, for better or worse, Zig has generics. That is something that cannot be ignored. The presence of generic capabilities means that generic code will be written; most of the std relies on generic code.
- frangfarang 2y ago[dead]
- ibraheemdev 2y ago> The message has some weird mentions in (alloc565), but the actual useful information is there: a pointer is dangling. The allocation ID is actually very useful for debugging. You can actually use the flags `-Zmiri-track-alloc-id=alloc565 -Zmiri-track-alloc-accesses` to track the allocation, deallocation, and any reads/writes to/from this location.
- k0tran 2y ago[dead]
- dzogchen 2y agoC and C++ both have support for bit-fields. https://en.wikipedia.org/wiki/Bit_field#Examples https://en.wikipedia.org/wiki/Bit_field#Examples https://fbb-git.gitlab.io/cppannotations/cppannotations/html/index.html https://fbb-git.gitlab.io/cppannotations/cppannotations/html... https://en.cppreference.com/w/cpp/language/bit_field https://en.cppreference.com/w/cpp/language/bit_field
- dolmen 2y agoHowever they lack support for pointers to those fields. Which zig has.