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.
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.
It appears part of the controversy surrounding Rust, is that many are of the opinion that it's not worth it because of the limited use case, poor readability, complexity, long compile times, etc... and that appears to be what certain advocates of Rust are not understanding or appreciating the difference in opinions. Rust is fine for them, specifically, but not for everyone.
A simpler performant language like Zig, or a boring language + a different architecture would have been the better choice.
And Zig is better when integrating with C/C++ libraries.
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!
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.
> 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.
Having variables with scopes that last longer than they're actually used and with names that are overly long and verbose leads to unpredictable bugs, too, when people misuse the variables in the wrong context later.
When I have `initial_foo`, `foo_feature_a`, and `foo_feature_b`, I have to read the entire code carefully to be sure that I'm using the right `foo` variant in subsequent code. If I later need to drop Feature B, I have to modify subsequent usages to point back to `foo_feature_a`. Worse, if I need to add another step to the process—a Feature C—I have to find every subsequent use and replace it with a new `foo_feature_c`. And every time I'm modifying the code later, I have to constantly sanity check that I'm not letting autocomplete give me the wrong foo!
Shadowing allows me to correctly communicate that there is only one `foo` worth thinking about, it just evolves over time. It simulates mutability while retaining all the most important benefits of immutability, and in many cases that's exactly what you're actually modeling—one object that changes from line to line.
When you have only one `foo` that is mutated throughout the code you are forced to organize the processes in your code (validation, business logic) based on the current state of that variable. If your variables have values which are logically assigned you're not bound by the current state of that variable. I think this a big pro. The only downside most people disagreeing with me are mentioning is related to ergonomics of it being more convenient.
If you allow shadowing, then you rule out the possibility of the value being used later. This prevents accidental use (later on, in a location you didn't intend to use it) and helps readability by reducing the number of variables you must keep track of at once.
If you ban shadowing, then you rule out the possibility of the same name referring to different things in the same scope. This prevents accidental use (of the wrong value, because you were confused about which one the name referred to) and helps readability by making it easier to immediately tell what names refer to.
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.
for i in range(N) {
for i in range(M) {
# Typo; wanted j.
# The compiler should complain.
}
}I know “use shorter functions” but tell that to my coworkers.
var age = get_string_from_somewhere();
var age = parse_to_int(age);
Without same-scope shadowing you end up with the obnoxious: var age_string = get_string_from_somewhere();
var age = parse_to_int(age_string);
Note that your current language probably does allow shadowing: in nested scopes (closures).This is a distinctly Go problem, not a problem with shadowing as a concept. In Rust you'd have to accidentally add a whole `let` keyword, which is a lot harder to do or to miss when you're scanning through a block.
There are lots of good explanations in this subthread for why shadowing as a concept is great. It sounds like Go's syntax choices make it bad there.
My position on shadowing is that it's a thing where different projects can have different opinions, and that's fine. There are good arguments for allowing shadowing, and there are good arguments for disallowing it.
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.
Either way you need to fulfill the contract, but I'd much prefer to find out I failed to do that at compile time.
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:
> 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?
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.
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. (They behave like ocamls polymorphic variants - https://ocaml.org/manual/5.3/polyvariant.html) This largely avoids the problems described in 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). 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.
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.
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 }
https://en.wikipedia.org/wiki/Bit_field#Examples
https://fbb-git.gitlab.io/cppannotations/cppannotations/html...
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.
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.
Saying Zig has generics because it has comptime is like saying c has generics, because C has a pre-processor
It's a wild take that you have to willfully ignore the context and nuance (implemention, rules, and semantics) for it to be true. More true than misleading, at any rate.
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.
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.
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/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/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.
On that scale COBOL is a better programming language.
> 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.
No, they're not. Rust "boxed traits" are, but those aren't what the author means.
> 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.
The standard library not being equipped to handle Unicode is the entire problem. Not solving it doesn't avoid the issue: it just makes Unicode safety the programmer's responsibility, increasing the complexity of the problem domain for the programmer and leaving more room for error.
Zig: I want to be a safer C
C: I don't have string type
Zig: No… not like that!
what? unicode is in the standard library.
https://github.com/ziglang/zig/blob/master/lib/std/unicode.z...
languages are actually really inconsistent on what they count as a unicode character: https://hsivonen.fi/string-length/
(I don't broadly disagree with you on unicode support, just linking an article relevant to that claim)
defer ties some code to a static scope. Destructors are tied to object lifetime, which can be dynamic. For example, if you want to remove some elements from an ArrayList of, say, strings, the string's would need to be freed first. defer does not help you, but destructors would.
``` defer { for (list.items) |str| gpa.free(str); list.deinit(gpa); } ```
When it's spelled out like this, it becomes obvious to the reader that maybe this is the wrong allocation strategy. Maybe the whole thing should go in an Arena. Or, similarly, maybe there should be an ArrayList that holds all the character data that your string ArrayList indexes into with a u32 (or points to with pointers, if you want to update all the pointers on resize). Regardless, I'd be skeptical of code where each string has a separate lifetime even though all the lifetimes could be tied.
Rust makes classic (bad) allocation strategies automatic. Zig makes good allocation strategies more attractive than classic (bad) allocation strategies.
More succinctly: Rust makes bad code safe, Zig makes good code easy.
Each rune may be comprised of various Unicode characters, which may themselves be 1-4 bytes (in the case of utf-8 encoding).
The one problem I have with this approach is that all of the categorization features operate a level below the runes, so you still have to break them up. The biggest drawback is that, at least in my (admittedly limited) research, there is no such thing as a "base" character in certain runes (such as family emojis- parents with kids). You can mostly dance around it with the vast majority of runes, because one character will clearly be the base character and one (or more) will clearly be overalys, but it's not universal.
Not sure about C#, but in Go for example ranging strings ranges over runes, but indexing pulls a single byte. And len is the byte length rather than rune length.
So basically it's a byte array everywhere except ranging. I guess I would have preferred an explicit cast or conversion to do that instead of by default.