Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Has anyone tried Zig? From reading the project page I don't see anything that it has/plans over something like Rust that's much further along.


Rust seems like a spiritual successor to C++. Zig seems like a spiritual successor to C (except it's a little closer to C, as you can directly run things from C in Zig)


Came here to say the same thing. I think there is probably room for both. Seems like the are valid use cases where you might want something with completely manual management like C, but without footguns like undefined behaviour and c-style strings.


Error management is different, and quite uniquely done. Zig deals with failure modes in a much more fine-grained way than Rust, which favours the algebraic-wrapper approach (Result / Option types). If you're writing actual, low-level systems code, your code might need to react differently depending on the failure mode: you need to know if it was an I/O issue, or out-of-memory, or disk-full, or etc. At the call point, Zig lets you enumerate all of the possible failure reasons comprehensively, and handle each one specifically if you choose to. This enumeration is produced and checked by the compiler, so you can't miss any cases. (It also lets you disregard the specifics if you don't care.)


That sounds quite similar to Rust, except less fine-grained than Rust. The Result type has an Error parameter which is usually an enumeration of the possible failure conditions, e.g. https://doc.rust-lang.org/std/io/struct.Error.html . That is, an IO function returns only IO errors, as compared to Zig's strategy which unifies all declared errors in the program into a single "error" enumeration.

(To be clear, there's a lot of cases when this precision isn't so useful, but the contention here is how fine grained they can be.)


There's definitely there's a distinction here between the approach you've described (Rust can indicate that a function returns only IO errors -- but doesn't tell you which subset of "all IO errors" might actually occur at this call point) and Zig's (the function call can result in an arbitrary set of specific errors, some of which might be IO and some might not; and only the actually possible errors are included in the set).

Is one better than the other? I doubt that question has an objective, one-size-fits-all answer. I'm happy to see experimentation in this domain! My point really is just that Zig is different from Rust in how it handles errors, even if there are some superficial similarities.


I think you haven't understood my point or maybe I haven't understood yours, and maybe it is because I have misunderstood Zig's approach.

My understanding is that something like `fn f() -> %T` returns a T or an `error`, and the latter can be literally any error value that exists anywhere in the program. It isn't restricted to the actual possible errors that might occur inside f, and thus has the same failings of Rust that you point out (don't know which subset of errors might be returned), except the unknown set of possible errors is likely to be larger.

(And, you can still get all the niceties of Rust's error handling infrastructure like the ? operator if you manually define an error type that is restricted to the exact set of possible errors: there's not a privileged type for how to represent errors.)


If there's any misunderstanding, it's probably mine. :) I'm reasonably familiar with Rust, and I understand your argument; but I only read about Zig a couple weeks ago, so my knowledge there is based only on a little reading and experimentation.

My reading of the release notes:

https://ziglang.org/download/0.2.0/release-notes.html

...is that error sets can be inferred (at both call-site and in the function declaration); and that inferred error-sets on return types are exact, in the sense that an inferred set will only include the actually-possible errors encountered in the call-tree.

To be fair, I don't know whether a function can declare that it may fail with a given, explicitly-declared error-set, where that error-set includes errors that the function actually cannot produce. (E.g., I say I can throw a network-error code, but I perform no network calls.) If this is permissible (i.e, the compiler doesn't complain), then that undercuts my argument a bit. :) On the other hand (again from the release notes), most functions are encouraged to simply declare their return type as !T, where the ! represents "the inferred error-set for the function," and this seems to support my claim.


Zig's errors are actually different and the variants can be statically known. Error type returns are not usually specified since they can usually be inferred. They can be explicitly specified if needed [1].

For example, the following code

    fn errorOrAdd(a: u8, b: u8) !u8 {
        if (a == 2) return error.FirstArgumentIsTwo;
        if (b == 2) return error.SecondArgumentIsTwo;
        return a + b;
    }

    pub fn main() void {
        if (errorOrAdd(0, 0)) |ok| {

        } else |err| switch (err) {
        // will force a compile-error to see what errors we haven't handled
        }
    }
emits this error at compile-time:

    /tmp/t.zig:13:18: error: error.SecondArgumentIsTwo not handled in switch
    } else |err| switch (err) {
                 ^
    /tmp/t.zig:13:18: error: error.FirstArgumentIsTwo not handled in switch
    } else |err| switch (err) {
                 ^
You can use the global `error` type which encapsulates all other error types if needed, replacing `errorOrAdd` in the previous `main` with the following function

    fn anyError() error!u8 {}
now requires an else case, since it can be any possible error.

    /tmp/t.zig:13:18: error: else prong required when switching on type 'error'
        } else |err| switch (err) {
                     ^
This works pretty well and is very informative in most cases. The tradeoffs are it can make some instantiation of sub-types a bit clunky [2] and you need to avoid the global error type everywhere. The global error infests all calling functions and makes their error returns global as a result. You can however catch this type and create a new specific variant so there is a way around this for the caller at least.

Do remember that Rust allows passing information in the Err variant of a Result while Zig's error codes are just that, codes with no accompanying state.

[1] https://github.com/tiehuis/zig-bn/blob/3d374cffb2536bce80453...

[2] https://github.com/tiehuis/zig-deflate/blob/bb10ee1baacae83d...


> Zig's errors are actually different and the variants can be statically known. Error type returns are not usually specified since they can usually be inferred. They can be explicitly specified if needed [1].

Yep, as a sibling points out, I was working with a long-ago version of Zig's error handling.


ErrorSets have been in the language for some time now. The syntax you are referencing is pretty outdated now too, when considering the pace of language changes here in Zig's pre-1.0 days.

https://ziglang.org/documentation/master/#Error-Set-Type


Ah that explains it: I was looking at the now-old https://andrewkelley.me/post/intro-to-zig.html#error-type


Let me introduce you to the concept of ErrorKind in Rust (i.e. in the IO module of the std): https://doc.rust-lang.org/std/io/enum.ErrorKind.html

There is also the "failure" crate which has patterns for the kind of thing you mentioned:

https://boats.gitlab.io/failure/error-errorkind.html https://boats.gitlab.io/failure/custom-fail.html


I don't believe that either ErrorKind or 'failure' gives the same degree of automated enumeration and checking that Zig provides. I'm not a Zig apologist... I just think you should read through the Zig documentation and examine the differences for yourself.

https://ziglang.org/documentation/master/#Errors

https://ziglang.org/download/0.2.0/release-notes.html (see "Error Sets")


If I'm understanding correctly, you can use enums in rust to recreate the same, though it would be slightly more verbose.

    enum MyErrorSet {
        NotFound(ErrorKind),
        SomethingElse(ErrorKind),
    }
If you don't care about propagating the original ErrorKind then that's just

    enum MyErrorSet {
        NotFound,
        SomethingElse
    }


Yes, I think you're right. But the burden for generating and maintaining the enums is on the developer; whereas in Zig, they can be automatically inferred by the compiler.

I don't think the distinction is super important in application work (for example). But for systems code I can see the benefit of having the compiler tell you, with certainty, why a given function can fail (and why it can't). E.g., a good device driver should handle all its failure modes with care, rather than with a shotgun approach.


Is there something about Rust that precludes it from reacting to failure modes as you describe?


I'm guessing, but I think the tricky part for Rust might be the automated construction of the Error-union type. E.g., at a certain call-point, the full union of all possible errors is ErrorSet<E1, E2, E3, ..., En>. The compiler determines this union, not the programmer. At the next call point, the union may be entirely different. To my best knowledge, Rust doesn't currently allow for variadic type constructors (i.e., where the number of parameters may be knowable at compile time, but the arbitrary-length parameter list cannot be expressed in the abstract syntax of the language).

https://ziglang.org/documentation/master/#Errors

edit: when I say "full union of all possible errors", I just want to be clear that this means "all the possible errors that could occur at this call point, and no others." It's not just a bag of "all possible errors that the language/library declares to exist."

edit #2: On second thought, I guess it might be possible for a Rust tool to build these types, although I think it would require changes to the language implementation. Variadic type constructors aren't necessarily needed. Another approach would be that every (type-specialized) function returns a Result<T, ES> type, where ES is some type that implements an "ErrorSet" kind, and where ES is very possibly unique to the function (that is, no other function might return Result<ES>). For example function A() might fail due only to two cases (say, out-of-memory or disk-full, but never network-error), and function B() might fail only on disk-full. Then the ErrorSet type for A has two constructors (OOM and DiskFull), where the ErrorSet type for B has only one (DiskFull). You could then use Rust's (excellent) pattern matching to exhaustively handle all cases (i.e., the specific constructors of that ErrorSet type) at the call site.

So the tricks would be:

1. Instrument the language to tag each specialized function with its error-set; this is computed recursively by examining the failure modes in the function body, as well as the error-sets of every function it might itself call, and taking the union of those sets. (Side note: I believe that, in Zig, a type-specialized function maps to a single error-set -- i.e., the error-set doesn't depend on runtime arguments in any way. But I could be wrong about this.)

2. Generate a potentially new ErrorSet-ish type for each function, or more spefically, for each error-set that is generated using the process in step #1. (I suppose you would have to give the type a name, but maybe it could be an anonymous internal type if there is language support for that.)

3. Then you could naturally "match" on the function's return value, and know that you've covered (exactly) the possible failure modes.

But this is just armchair-quarterbacking on my part. I don't know the internals of the Zig error system, nor what changes Rust would actually need.


I write a lot of Zig. In fact, all my hobby projects are in Zig since I've started using it because I like it so much.

I haven't tried Rust, but I've seen enough of it to know I'd hate it. Where Zig has a clear advantage is that it is vastly smaller and simpler and does not enforce a memory management paradigm on you. There isn't even a default allocator.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: