Hacker News new | ask | show | jobs
by armchairhacker 1046 days ago
My understanding is that Zig has all of the power and modernity of Rust, without the strictness and borrow checker. Unlike Rust, it also has powerful compile-time evaluation and custom allocators, and probably more improvements I'm not familiar with (in Rust you can effectively emulate custom allocators, but you have to rewrite every allocating structure to use them; or you can use nightly, but most third-party library and even some standard-library types don't support them).

I also heard someone say "Zig is to C what Rust is to C++". Which I interpret as, it's another maximum-performance modern language, but smaller than Rust; "smaller" meaning that it has less safety and also abstraction (no encapsulation [1]), but less requirements and complexity.

Particularly with games, many devs want to build a working prototype really fast and then iterate fast. They don't want to deal with the borrow checker especially if their code has a lot of complex lifetime rules (and the borrow checker is a real issue; it caused evanw to switch esbuild to Go [1]). In small scripts with niche uses, safety and architecture are a waste of effort, the script just has to be done and work (and the latter is only partly necessary, because the script may not even be fed enough inputs to cover edge cases). Plus, there are plenty of projects where custom allocation is especially important, and having every type support custom allocation is a big help vs. having to rewrite every type yourself or use `no_std` variants.

[1] https://github.com/ziglang/zig/issues/2974

[2] https://github.com/evanw/esbuild/issues/189#issuecomment-647...

3 comments

>My understanding is that Zig has all of the power and modernity of Rust, without the strictness and borrow checker.

This is an oxymoron :) The strictness and borrow checker are part of the power and modernity of Rust.

But even apart from that, Rust has automatic value-based destructors (destructor follows the value as it's moved across scopes and is only called in the final scope), whereas Zig only has scope-based destructors (defer) and you need to remember to write them and ensure they're called exactly once per value. Rust has properly composable Option/Result monads, whereas Zig has special-cased ! and ? which don't compose (no Option of Option or Result of Result) but do benefit from nice built-in syntax due to their special-cased-ness. Rust has typed errors whereas Zig only has integers, though again that allows Zig to have much simpler syntax for defining arbitary error sets which would require defining a combinatorial explosion of enums in Rust.

Of course from Zig's point-of-view these are features, not deficiences, which is completely understandable given what kind of language it wants to be. And the other advantages you listed like comptime (Rust's const-eval is very constrained and has been WIP for ages) and custom allocator support from day 1 (the way Rust is bolting it on will make most existing code unusable with custom allocators, including parts of Rust's own standard library) are indeed good advantages. Zig also has very nice syntax unification - generic types are type constructor functions fn(type) -> type, modules are structs, etc.

I hope that one day we'll have a language that combines the best of Rust's strictness and Zig's comptime and syntax.

> Rust has properly composable Option/Result monads, whereas Zig has special-cased ! and ? which don't compose (no Option of Option or Result of Result)

?!!??u32 is a perfectly cromulent type in Zig.

How do you express the difference between None and Some(None) in Zig?
I've found that types like these normally come up in generic contexts, so the code I'm writing only usually deals with one layer of Option or Result until I get to the bit where I'm actually using the value and find out I have to write "try try foo();". That said, I think this sort of thing will do it:

  const std = @import("std");
  fn some(x: anytype) ?@TypeOf(x) {
    return x;
  }
  fn print_my_guy(maybe_maybe_x: ??u32) void {
    if(maybe_maybe_x) |maybe_x| {
      if (maybe_x) |x| {
        std.debug.print("that's Some(Some({d})) {any}\n", .{x, maybe_maybe_x});
      } else {
        std.debug.print("that's Some(None) {any}\n", .{maybe_maybe_x});
      }
    } else {
      std.debug.print("that's None {any}\n", .{maybe_maybe_x});
    }
  }
  pub fn main() void {
    const a: ??u32 = 5;
    const b: ??u32 = null;
    const c: ??u32 = some(@as(?u32, null));
    print_my_guy(a);
    print_my_guy(b);
    print_my_guy(c);
  }
What about

  const std = @import("std") ;
  const print = std.debug.print ;
  
  pub fn main() void {
  
    const simple_none : ?u8 = null ;
  
    const double_optional_none : ??u8 = null ;
  
    const double_optional_some_none : ??u8 = simple_none ;

    print("none equals some none: {}", .{ double_optional_none == double_optional_some_none });
    // prints none equals some none: false
  
  }
> Rust's const-eval is very constrained and has been WIP for ages

Having strong backwards compatibility does that to the language, alternative is arguably worse (see Python 2 vs 3).

Yes, and I don't want a backward-incompatible Rust 2.0 either, but the slowness of stabilizing ! (named for when it's going to be stabilized), specialization, TAIT, const-eval, Vec::drain_filter, *Map::raw_entry, ... is annoying. Also the lack of TAIT currently causes actual inefficiency when it comes to async traits, because currently async trait methods have to return allocated virtualized futures instead of concrete types. Same for Map::raw_entry, without which you have to either do two lookups (`.get()` + `.entry()`) or always create an owned key even if the entry already exists (`.entry(key.to_owned())`).
If you think, that's bad, look at C/C++ standardization bodies, where stuff is eternally blocked because ABI compatibility.

---

Problem is lots of implementation things are vying for inclusion. And many thing people want aren't safe or block/are blocked by possible future changes.

For example default/named arguments were blocked by ambiguity in parsing when it comes to type ascription iirc. And not having default arguments makes adding allocator quite more cumbersome.

Plus Rust maintainers are seeing some common patterns and are trying to abstract over them - like keyword generics/ effect system. If they don't hit right abstraction now, things will be much more harder later. If they over abstract, its extremly hard to remove it.

---

Slowness of stabilizing never type (!) and specializations has to do with the issues they cause mainly unsoundness and orphan rules issues, iirc I haven't checked them in a while.

But otherwise yeah, Unstable book keeps growing and growing: https://doc.rust-lang.org/unstable-book/index.html

Also none of the common knowledge around traditional data-structures and algorithms work with Rust anyways. One needs to dance like a ballerina with their hands and feet tied.
“Linked lists are hard” is not “none of the common knowledge around traditional data-structures and algorithms work”.
I retort that almost all of the concurrent data structures are obnoxious to represent in Rust.

A lock-free ConcurrentHashMap, for example, is by no means a straightforward data structure in a non-GC language. Even if you somehow dodge Rust's pedantry, you still have to figure out who owns what, who pays for what and when they pay for it--and there are multiple valid choices!

Non-GC allocation/deallocation in concurrent data structures probably still qualifies as a solid CS problem.

(And, before you point me to your favorite crate for ConcurrentHashMap, please check it's guarantees when one process needs to iterate across keys while another process simultaneously is inserting/deleting elements. You will be shocked at how many of them need to pull a lock--so much for lock-free.)

This is partially why allocation is left to the caller (concurrent data structures are often intrusive), have single consumers (made lock free using separate synchronization) or only have lockless guarantees not obstruction freedom.

I think the obnoxious part in Rust is doing intrusive and shared mutability parts of data structures. Having to go between NonNulls, Options, Pin, and Cell/UnsafeCell is not a pleasant experience.

You should always know who owns some data, even if you aren't using Rust!
Garbage collection means the VM owns it! And that’s great. How it should be.
Welcome to a land of memory bloat because you've got a chain to a reference due to a lack of clear ownership.
I never understood this take. You shouldn't be heap allocating each node in your linked list anyways. It's trivial to convert the pointer fields to indexes and have each nodes live in a `Vec` unless you need it to be intrusive. You'll get better performance anyways because you're not doing a pointer deref and blowing your TLB up every time you traverse the list.
Ever tried to implement generic trees in Rust?
Trees are easy. It's the desire to have backreferences - making it a graph - that kills you.
I think Rust and Zig are very different in principals and shouldn’t really be compared. It’s almost like comparing C and Ada.