A blog post I came across describing the experience of an experienced Rust (and other languages) programmer trying Zig for the first time. Felt like the author approached it with a good attitude.
I am not the author.
A blog post I came across describing the experience of an experienced Rust (and other languages) programmer trying Zig for the first time. Felt like the author approached it with a good attitude.
I am not the author.
All told, this reflects each languageās design goals and target domain, and itās a reasonable trade-off but subjectively, I found the resulting Zig code less readable than its Rust counterpart.
Honestly, how? I do understand what rust counterpart means and do, but, to me, zig is way more clear and readable in the given examples. Is there anyone feels the same as the author, if so, can you explain your perspective?
EDIT: I am genuinely curious, I donāt want to come across as dissing the author, or anything like that. I am glad that he had a positive experience.
I find some of the functional examples to be simpler to understand.
When you use functional programming concepts you can reason about them and use them like building blocks for behavior, so youāre inherently a bit higher on the abstraction hierarchy than with zigās imperative approach.
I think it comes down to background. I have a background in electronics/embedded so for me the more bare-metal approach is overall more intuitive. Iād imagine that if someone came from a math background theyād find the functional examples much simpler.
Sometimes you need to actually be able to write code as quickly as you can think of it, nothing I can think of beats functional programming for this. (Maybe something stack based?)
Great writeup! Nice to see people with a similar background ![]()
It is always the language server
I switched from Rust to Zig last year and for me its the same as you feel: Zig made me really understand some things, especially the basic ones and just feels much more straightforward. Rusts method chaining (functional style?) might be faster sometimes, but hides most stuff behind those methods. The same accounts for memory management. I tried Rust again some weeks ago and it felt over complicated and like it carries a big overhead alone due to all the crates you simply fetch.
But thatās only my personal experience. Plus, I have to add that Iām no trained professional programmer who has decades of experience programming in C, Rust, C++ etc.
Edit:
Thatās true too. Rusts LSP is somehow too powerful. So you donāt run into compile errors that often. But compile errors teached me a lot in Zig and made me read much source code of std lib. Thus, thanks to zls for being good and helpful but not too perfect ![]()
@matklad suffering from success ![]()
I think method chaining in Rust is often unfairly criticised for hiding stuff.
yes, idiomatic Rust often includes a lot of method chaining, with return types too complex to write down aka iterators of iterators of futures of results of options etcā¦
Itās true that this method chaining is not explicit regarding allocation, but itās pretty clear and well documented which methods allocate and which donāt. If you ask any seasoned Rust developer where method chains allocate, call IO, etc., they can pinpoint that to you with ease.
In Zig (if code follows conventions) you always know if a method can allocate, but you donāt know if it will until you read source code or docs, much like Rust.
Also, because Rust has traits for Iterators, Streams etc., common allocation operations on method chaining always follow the same predictable APIs (e.g .collect() etc.), so you generally donāt have to implement or document any of these methods yourself, you just have to compose them together.
![]()
We are working on one component of Zig IDE space at the moment: #compiler > Zig Resilient Parser @ ![]()
Seems a bit unfair to only attempt to do Rust patterns in Zig.
One of these days, I want to do a Rust vs Zig comparison where I implement a simple AVL tree. Trivial in Zig, but Rust will then have me drown in a hellscape of lifetime annotations, the result of naively accepting the suggestions of the compiler when it doesnāt compile. And I am forced to chase my own tail as it tells me to remove the very things it told me to add moments before, and the code slowly turns into an inscrutable mess of symbols that a casual glance over my shoulder by a stranger will have them conclude Iāve been possessed and am attempting to contact an eldritch monster through the computer.
In my opinion, the culture that formed around avoiding unsafe really hurt that part of the language, and seeing a relative lack of interest in improving the experience in unsafe was discouraging, particularly if you want to write data structures.
That being said, Iām a little skeptical itās so bad to implement any kind of tree structure, as long as you donāt have backlinks. A singly linked list is dead easy, or a pointerful AST. The standard library has a BTree. It might have more friction than zig, but my gut says in those cases itās good friction. This is less the case if you are implementing something on top of an array, since you may need to deal with MaybeUninit and controlling when the implicit calls to Drop::drop happen.
macro_rules! and especially proc_macros do suck compared to comptime, even though I liked them quite a bit. I tried to implement MultiArrayList and I donāt think I got far at all, since writing proc_macros is pretty daunting. Zig is the clear winner for metaprogramming and reflection.
Nah, as I explicitly wrote, its my purely personal experience. Thus, its not unfair. And Iām not one of those seasoned Rust devs.
For me many things in Zig are just clearer. E.g. I made a terminal file manager in Rust and now I remade it in Zig. For loading a regular dir, using Rusts standard Path functions its making almost 10 time as many syscalls as the same with Zig (traced with strace). Thus, there is definitely some āhiddenā flow in calling these Rust functions and methods compared to Zigs, at least to me
(experienced Rust devs might be able to tell all those SYS calls)
Hey, thanks for responding!
re-reading my message in the morning, it doesnāt sound kind enough, so sorry for that.
Anyway, do you mind sharing one of those pieces of Rust code? Iām curious to see it (just to know if Iām a seasonned Rust developer myself).
If inappropriate to send here (since Rust and not Zig), please send it by message
Thanks
Hi, no problem. I didnāt feel insulted, just wanted to make clear its my personal view and no general criticism of Rust.
Sure you can compare them. Both are open source, of course:
But donāt expect idiomatic code. As stated Iām just a hobby dev and both projects have been/are designed for my very personal needs in the first place.
These are very important observations! My thinking is that implementing something like AVL tree is easy in an unsafe rust. I did rb 10 years ago (
) exactly as a learning exercise for unsafe
Unsafe Rust is more annoying than Zig, but itās not too bad, given small volume of unsafe code.
But thereās a catch. You need to know when to use unsafe, and when to avoid it. It is hard. You need to internalize āimpossible objectsā, like parent pointers, cyclic data structures, self-referential data structures, pinned memory, etc.
And what happens in practice is that a lot of people, when faced with a borrow checking error, think they just need unsafe. But the compiler is often right! So the community understandably develops a culture that āyou should be at least as tall as Gankra to write unsafe codeā. But then other people tie themselves into knots trying to do a safe AVL tree. Technically, the right answer is obviously using unsafe. But the social/culture problem of deploying that solution in just the right proportion is hard.
Thatās just āmethod chainingā though, not āfunctional styleā and both are pretty much equivalent when it comes to the generated code.
E.g. the above is the same as:
http_client.set_address(a);
http_client.set_port(p);
http_client.set_method(m)
http_client.add_content(c)
http_client.connect();
http_client.send();
ā¦or alternatively in old-school C-style (worst case for readability):
send(connect(add_content(set_method(set_port(set_address(http_client, a), p), m), c)));
ā¦of course nobody would actually do that in C, but instead:
set_address(http_client, a);
set_port(http_client, p);
set_method(http_client, m);
set_content(http_client, c);
connect(http_client);
send(http_client);
ā¦the main problem is stylistic, e.g. I find all the above forms a lot less appealing than āoption-bagā style like:
http_client.send(.{
.url = "https://bla",
.method = .GET,
.header = .{ ... },
.body = .{ ... },
});
Performance-wise, none of the above makes any significant difference (although I would be careful about using option-bags for very high frequency functions).
Iāve done some Rust too, and my experience was similar - for me, Zig was a breath of fresh air, not just because of meta-programming - I was suddenly able to think freely about the problem and to iterate, without having to rewrite everything from scratch each time, because I hit some lifetime wall, and figured out that I should do it differently, but now itās going to be multi-day effort to fix everything. LLMs might have changed that a bit but youād still have to review all of the code, and I also think that it is inevitably going to cause a messy code in the end (LLMs are overly safe and add a lot of noise and accidental complexity).
Unfortunately, there were many breaking changes since Zig 0.14, and thatās something that should be mentioned too, Zig is changing a lot and many people, me included, understimate the work and fatique of having to upgrade chain of multiple dependencies where often you need to fork and fix it yourself, not to mention that std is also changing a lot and I am now actively avoiding std where I can.
Regarding hidden allocations, I think the biggest issue is how Vec<T> and .clone() is often the easiest way out of trouble and so people do just that. My style has changed a lot since then but Rust was very interesting for me because it was FP friendly. Those FP patterns do not necessarily equal to allocations but often they do. And at that point it might be better to go with something with GC or even just use more optimized FP language.
Another option is to use arenas, but at least back at the time, they were awkward to use because of lifetimes. And it always felt to me that Rust lifetimes were rather kind of PoC, and they were hard to use in practice because there was no support for runtime branding.
I know there were some projects to explore that area but IMO the best attempt was what bevy came up with. But ECS does not fit to everything and people should not be forced to reach for ECS just because they need something like that.
Hm, sorry for slight OT ![]()
Just one note that the Rust builder pattern is not possible to do in Zig, because you canāt return by value and then call mutations, all chained in one expression. You need to have extra var which in my opinion sucks for readability.
BTW: I wanted to reply to general thread first and then reply to you here. Unfortunately, I donāt see any way to unlink my previous post from your thread.
Saying that it is not possible seems like a stretch. Of course, you either have to make the variable first as you said(which I really dont think is a deal breaker), or actually lean into the builder pattern and accept immutability (which I think would get optimized away anyway)
Either works perfectly fine in my opinion and I am having difficulties imagining this would not be good enough to replace whatever rust has.
const HttpClient = struct {
port: u32,
addr: u32,
const init: HttpClient = undefined;
fn withPort(self: HttpClient, port: u32) HttpClient {
var new = self;
new.port = port;
return new;
}
fn withAddr(self: HttpClient, addr: u32) HttpClient {
var new = self;
new.addr = addr;
return new;
}
};
pub fn main() !void {
const client = HttpClient.init.withPort(420).withAddr(42);
std.debug.print("{d} {d}", .{ client.addr, client.port });
}
const HttpClient = struct {
port: u32,
addr: u32,
fn setPort(self: *HttpClient, port: u32) *HttpClient {
self.port = port;
return self;
}
fn setAddr(self: *HttpClient, addr: u32) *HttpClient {
self.addr = addr;
return self;
}
};
pub fn main() !void {
var client: HttpClient = undefined;
_ = client.setAddr(42).setPort(420);
std.debug.print("{d} {d}", .{ client.addr, client.port });
}