Most active commenters
  • eatonphil(4)
  • 59nadir(4)

←back to thread

Zig is hard but worth it

(ratfactor.com)
401 points signa11 | 14 comments | | HN request time: 0.001s | source | bottom
Show context
helen___keller ◴[] No.36150396[source]
My main issue with Zig is that I’m scared to invest time in writing something nontrivial to see the community/adoption flounder then regret not using Rust or C++ later

The language itself is fun. The explicit-ness of choosing how allocation is done feels novel, and comptime is a clean solution for problems that are ugly in most languages.

Aside from lack of community I’d say the biggest nuisance is error handling in nearly everything including allocations. I get that allocation can fail but the vast majority of programs I write, I just want to panic on an allocation failure (granted these aren’t production programs…)

Edit: in retrospect, allocator error handling verbosity is probably necessary due to the power the language gives you in choosing allocators. If I use the general purpose allocator then my edge case is PC out of memory; if I use a fixed buffer allocator, a failed allocation is a code bug or a common error case, so we might want to handle the failed allocation and recover

replies(5): >>36150507 #>>36150918 #>>36151708 #>>36155513 #>>36158163 #
kuroguro ◴[] No.36150507[source]
I suppose you could write a few line wrapper that panics :)

But yeah, most of the time I don't even want to think which allocator to use let alone handle it's errors.

replies(2): >>36150581 #>>36153145 #
eatonphil ◴[] No.36150581[source]
This is basically what I've come to do in the Zig scripts I write at work.

It took a bit of getting used to when I joined but we agreed as a team to have all meaningful scripts written in Zig not bash (for one, bash doesn't work on Windows without WSL and we need to support Windows builds/testing/etc.).

It makes about as much sense as any other cross-platform scripting option once I got used to it!

Some examples:

Docs generation: https://github.com/tigerbeetledb/tigerbeetle/blob/main/src/c...

Integration testing sample code: https://github.com/tigerbeetledb/tigerbeetle/blob/main/src/c...

Running a command wrapped in a TigerBeetle server run: https://github.com/tigerbeetledb/tigerbeetle/blob/main/src/c...

replies(1): >>36151035 #
rowls66 ◴[] No.36151035[source]
I am truly puzzled by this. I understood Zig to be a very low level language like 'C'. Why would you write scripts in it?
replies(1): >>36151084 #
1. eatonphil ◴[] No.36151084[source]
It's significantly nicer to write than C (my opinion obviously). I see it as a general purpose language.

But mostly the team decided to do this because we wanted to unify on one language and double down on the investment in Zig.

I'm not a fanboy (nothing wrong if anyone is, just clarifying about myself); I think this choice was right.

replies(4): >>36151224 #>>36151600 #>>36152429 #>>36155338 #
2. vaughan ◴[] No.36151224[source]
I'm moving all my scripting to TypeScript using Bun (JS runtime written in Zig).

For scripting its a way better choice. `bun:ffi` also makes it trivial to run C or Zig code when you need to.

3. 59nadir ◴[] No.36151600[source]
For what it's worth I think this is an excellent choice. Back in 2019 I was deciding whether I wanted to pursue Zig full-time and one of the upsides that I determined was that once you reach critical mass writing all of your code for tools and things in Zig you end up with things that are really exactly what you need and with a very high baseline for speed, flexibility, and so on.

Right now I'm considering the same thing but with Odin and for many of the same reasons that I had for Zig; it's an excellent language to write foundational code in and once you do, you end up being able to build significantly more understandable, reliable, stable and consistent things.

This, but on a company level, is a real multiplier. Once you adopt a couple of Python scripts and that's OK, you give up the possibility of wielding this sharp spear.

Edit:

I think "How I program C" by Eskil Steenberg is an interesting window into what you can get if you laser focus on a language and environment and allow yourself to build up a mountain of code that you dogfood: https://www.youtube.com/watch?v=443UNeGrFoM

At some point I will likely soft-retire and at that point it's exceedingly unlikely that I'll bother with using other people's libraries except a few key ones that I think are decent, and at that point there really is no reason I'd sit down and write these fundamentals in anything but a lower-level language. Odin, Zig or something like it would pretty much be the only thing on the table.

replies(1): >>36161190 #
4. hoosieree ◴[] No.36152429[source]
Binary executables are a nice feature, especially for distributing to users.

It's much easier for most people to download a standalone "mac" or "windows" binary than to know if they already have the right version of Python or Perl or Clang (and all the transitive dependencies your project adds).

replies(1): >>36152503 #
5. eatonphil ◴[] No.36152503[source]
We don't host the built binaries of these scripts (we could!), but we bootstrap the local environment through a single bash script or batch script (Windows) that pulls down the Zig compiler. Then everything else in the repo depends only on that until we get until client-language specific bits which of course depend on other languages.

But yeah it is quite simple still.

6. rowls66 ◴[] No.36155338[source]
I guess I don't see C as programming language for writing scripts in either. In my view any language that requires a separate complication step is not a scripting language, and therefore not a language in which one writes scripts. In C or Zig you write programs.

Maybe I am just being too pedantic.

replies(3): >>36155537 #>>36159946 #>>36162472 #
7. eatonphil ◴[] No.36155537[source]
I didn't call it a scripting language. Nor do I think C would be great to write scripts in either. :) I only said we write scripts in Zig. But if you'd like to call these files programs instead of scripts then that's ok too!
8. Kamq ◴[] No.36159946[source]
A number of languages that would have been traditionally compiled (statically typed, produce a native binary by default, etc), have started adding a "run" command.

If your language compiles fast enough, it's about the same experience as running a python script.

9. QQ00 ◴[] No.36161190[source]
how your experience with Odin so far? How Odin differ from Zig in your opinion? What you likes/dislikes compared to Zig or other languages?
replies(1): >>36162056 #
10. 59nadir ◴[] No.36162056{3}[source]
I think Odin is terrifically designed overall. There are design choices that I was initially very skeptical about but when I decided to use the language they actually made a lot of sense.

Some overall differences between Odin and Zig and how I relate to them are:

## Exhaustive field setting

Zig requires you to set every field in a struct. Everything everywhere has to be initialized to something, even if it's `undefined` (which means it's the same thing as missing initialization in C, etc.).

Odin instead takes the position that everything is zero-initialized by default. It then also takes the position that everything has a zero value and that zero value is a valid value to have. The libraries think so, the users of the language think so and what you get when everything works like this is code that overwhelmingly talks exactly only about the things that need talking about, i.e. we only set the fields right now that matter.

Exhaustive field setting does remove uncertainty and I was very skeptical about just leaving things to zero-initialization, but overall I would say I prefer it. It really does work out most of the time and I've had plenty of bugs in Zig caused by setting something to `undefined` and just moving on, so it's not really as if that exhaustiveness check for fields was some 100% solution.

## An implicit context vs. explicit parameters

Zig is more or less known for using parameters to pass around allocators, and so on. It's not a new concept in most lower-level languages but it's one of the first languages to be known for baking this into the core community and libraries.

Odin does the same thing except it uses an implicit parameter in all Odin functions that is called `context`. When you want to change the allocator for a scope, you only need to set `context.allocator` (or `context.temp_allocator`) and every function you call in that scope will use that allocator. We can also write functions that take an optional parameter that defaults to the current allocator:

    join :: proc(
      a: []string,
      sep: string,
      allocator := context.allocator,
    ) -> (
      res: string,
      err: mem.Allocator_Error,
    ) {
    }
This way we get the same behavior and flexibility of talking about allocators but we can also default to either the basic default or whatever the user currently has in scope. We *can* also be more explicit if we want. The ability to have this implicit again makes it so we only need to talk about the things that are special in the code.

The context is also used for logger information, by the way, and you also have a `user_data` field that can be used to hold other stuff but I haven't really needed it for anything so far.

## Error information

Zig is known for its error unions, i.e. a set of error codes that can be inferred and returned from a function based on the functions it calls. These are nice and undoubtedly error handling in Zig is very neat because of it. However, there is a major downside: You can't actually attach a payload to these errors, so these are just tags that you propagate upwards. Not a huge deal but it is annoying; you'll have to have a parameter that you fill in when an error has occurred and you need more info:

    // `expect_error` here is only filled in with something if we have an error
    pub fn expect(self: *Self, expected_token: TokenTag, expect_error: *ExpectError) !Token {
    }
Odin instead has a system that works much the same, we can return early by using what is effectively the same as `try` in Zig: `or_return`. This will check the last value in the return type of the called function to see if it's an error value and return that error value if it is.

The error values that we talk about in Odin are just its normal values and can be tagged unions if we so choose. If we have the following Zig definitions for the `ExpectError` example:

    pub const ExpectTokenError = struct {
        expectation: TokenTag,
        got: Token,
        location: utilities.Location,
    };
    
    pub const ExpectOneOfError = struct {
        expectations: []const TokenTag,
        got: Token,
        location: utilities.Location,
    };
    
    pub const ExpectError = union(enum) {
        token: ExpectTokenError,
        one_of: ExpectOneOfError,
    };
We could represent them as follows and just use them as the error return value without holding a slot as a parameter that we will fill in:

    ExpectTokenError :: struct {
      expectation: TokenTag,
      got: Token,
      location: Location,
    }
    
    ExpectOneOfError :: struct {
      expectations: []TokenTag,
      got: Token,
      location: Location,
    }
    
    ExpectError :: union {
      ExpectTokenError,
      ExpectOneOfError,
    }
    
    expect_token :: proc(iterator: ^TokenIterator, expected_token: TokenTag) ->
        (token: Token, error: ExpectError) {
    }
They are normal tagged unions and in contrast to Haskell/Rust unions we don't have to bother with having different constructors for these just because a type is part of several different unions either, which is a big plus. We still get exhaustiveness checks, something akin to pattern matching with `switch tag in value { ... }`[0] and so on. Checking for a certain type in a union also is consistent across every union that contains that type, which is actually surprisingly impactful in terms of design, IMO.

## Vectors are special

This isn't going to have a Zig equivalent because, well, Zig just doesn't.

Odin has certain names for the first, second, third, etc., positions in arrays. This is because it's specifically tailored to programmers that might deal with vectors. It also has vector and matrix arithmetic built in (yes, there is a matrix type).

    array_long := [?]f32{1, 2, 3, 4, 5}
    array_short := [?]f32{0, 1, 2}

    fmt.printf("xyzw: %v\n", array_long.xyzw)
    fmt.printf("rgba: %v\n", array_long.rgba)
    fmt.printf("xyz * 2: %v\n", array_long.xyz * 2)
    fmt.printf("zyx + 1: %v\n", array_long.zyx + 1)
    fmt.printf("zyx + short_array: %v\n", array_long.zyx + array_short)
This gives the following output:

    > odin run swizzling
    xyzw: [1.000, 2.000, 3.000, 4.000]
    rgba: [1.000, 2.000, 3.000, 4.000]
    xyz * 2: [2.000, 4.000, 6.000]
    zyx + 1: [4.000, 3.000, 2.000]
    zyx + short_array: [3.000, 3.000, 3.000]
It seems like a small thing but I would say that this has actually made a fair amount of my code easier to understand and write because I get to at least signal what things are.

## Bad experiences

### Debug info

The debug information is not always complete. I found a hole in it last week. It's been patched now, which is nice, but it was basically a missing piece of debug info that would've made it so that you couldn't know whether you had a bug or the debug info was just not there. That makes it so you can't trust the debugger. I would say overall, though, that the debug info situation seems much improved in comparison to 2022 when apparently it was much less complete (DWARF info, that is, PDB info seems to have been much better overall, historically).

0 - https://odin-lang.org/docs/overview/#type-switch-statement

replies(2): >>36163988 #>>36238976 #
11. 59nadir ◴[] No.36162472[source]
I think the idea here is that "small programs" = "scripts". Modern lower level languages are mostly terrific for writing small programs, which obviates the need for scripting languages.
12. QQ00 ◴[] No.36163988{4}[source]
Thank you so much. It look like Odin is really a beautiful language to program with.

Did you encounter memory bugs? Essentially what memory safety feature Odin offer? (From what I read here, Zig and Rust offer some features to eliminate entire classes of bugs which remove the nightmare of hours of debugging)

Anything you dislike or wish Odin has that other languages offer?

>that the debug info situation seems much improved in comparison to 2022 when apparently it was much less complete (DWARF info, that is, PDB info seems to have been much better overall, historically).

I believe Odin was developed on windows and other system come later, probably that why PDB is much better.

replies(1): >>36167353 #
13. 59nadir ◴[] No.36167353{5}[source]
> Did you encounter memory bugs? Essentially what memory safety feature Odin offer? (From what I read here, Zig and Rust offer some features to eliminate entire classes of bugs which remove the nightmare of hours of debugging)

Odin is essentially in the same ballpark as Zig in terms of general safety features. Slices make dealing with blocks of things very easy in comparison to C, etc., and this helps a lot. Custom allocators make it easy to segment your memory usage up in scopes and that's basically how I deal with most things; you very rarely should be thinking about individual allocations in Odin, in my opinion.

> Anything you dislike or wish Odin has that other languages offer?

I would say that in general you have to be at least somewhat concerned with potential compiler bugs in certain languages and Odin would be one of them. That's not to say that I've stumbled on any interesting compiler bugs yet, but the fact that they very likely do exist because the compiler is a lot younger than `clang` or `gcc` makes it something that just exists in the background. Multiply that by some variable amount when something is more experimental or less tried and true. The obvious example there is the aforementioned debug info where on Linux this has been tried less so it is more likely to be worse, and so on.

In an ideal (fantasy) world I'd love something like type classes (from Haskell) in Odin; constraints on generic types that allow you to write code that can only do exactly the things expressed by those constraints. This gives you the capability to write code that is exactly as generic as it can logically be but no more and no less. Traits in Rust are the same thing. With that said, I don't believe that neither Haskell nor Rust implements them in a way that doesn't ruin compile time. Specialization of type classes is an optimization pass that basically has to exist and even just the fact that you have to search all your compiled code for an instance of a type class is probably prohibitively costly. It's very nice for expression but unless Odin could add them in a way that was better than Haskell/Rust I don't think it's worth having.

I would like to see how uniform function call syntax would work in Odin but this is actually addressed in the FAQ here: https://odin-lang.org/docs/faq/#why-does-odin-not-have-unifo...

UFCS works really well in D but D also has ad-hoc function overloading whereas Odin has proc groups. I think UFCS only really works with exceptions as well, so I think it can become awkward really fast with the amount of places you want to return multiple values where your last one represents a possible error.

14. Tozen ◴[] No.36238976{4}[source]
> Odin instead takes the position that everything is zero-initialized by default...

> Exhaustive field setting does remove uncertainty and I was very skeptical about just leaving things to zero-initialization, but overall I would say I prefer it. It really does work out most of the time...

Vlang does this as well (also influenced by Wirth/Pascal/Oberon/Go). Overall, this is an advantage and greater convenience for users.