Live data from Hacker News

Zig's new plan for asynchronous programs

lwn.net

181–190 of 274 posts

Re: Zig's new plan for asynchronous programs

#181

[flagged]

I don’t know the details but reading the article they got this right. It’s been my main gripe with Rust which imo totally botched it. Or rather, they botched the ergonomics. Rust still allows low level control just fine… (but so does a plain old explicit event loop). Go did much better, succeeding at ergonomics but failing at low level control (failed successfully that is, it was never a goal).

The trick to retaining ergonomics and low level control is precisely to create a second layer, a ”runtime” layer, which is responsible for scheduling higher level tasks, IO and IPC. This isn’t easy, but it’s the only way. Otherwise you get an interoperability problem that the coloring and ecosystem fragmentation in Rust reflects.

Re: Zig's new plan for asynchronous programs

#182
post #97

Earlier quoted context omitted.

What about it? It gets called without an Io parameter. Same way that a function that doesn't allocate doesn't get an allocator. I feel like you're trying to set me up for a gotcha "see, zig does color functions because it distinguishes functions that do io and those that don't!". And yes, that's true. Zig, at least Zig code using std, will mark functions that do Io with an Io parameter. But surely you can see how tha…

This creates the drill-down issue we see with React props where we have to pass objects around in the call chain just so that somewhere down the line we can use it. React gets around this with the context hook and which you can access implicitly if it has been injected at a higher level. Do you know if Zig supports something of the sort?

I think (and I’m not a Zig user at anything above a hobbyist level) based on what the developers have discussed publically:

React has a ‘roughly’ functional slant to the way it does things and so needs to provide a special case ‘hook’ for a certain type of context object. Zig however is an imperative language that allows for global state (and mutable global state for that matter), which means that there is always a way to access global variable, no hook required. On the other hand, I am relatively certain (almost 100% to be honest) there can not be a context/IO , or any data/variable, passed into a function higher up the call stack and have that propagate to the lower level via implicit inclusion.

Re: Zig's new plan for asynchronous programs

#183

Earlier quoted context omitted.

Can you explain for those of us less familiar with Haskell (and monads in general)?

Reader monad is a fancy way of saying ‘have the ability to read some constant value throughout the computation’. So here they mean the io value that is passed between functions.

Well I don't think that fits at all. In Zig, an Io instance is an interface, passed as a parameter. You can draw some connections between what Zig is doing and what Haskell is doing but it's not a monad. It's plain old interfaces and parameters, just like Allocator.

Re: Zig's new plan for asynchronous programs

#184
post #135

Earlier quoted context omitted.

This creates the drill-down issue we see with React props where we have to pass objects around in the call chain just so that somewhere down the line we can use it. React gets around this with the context hook and which you can access implicitly if it has been injected at a higher level. Do you know if Zig supports something of the sort?

It doesn't and likely never will. This has been a non-issue for years with Allocator. I fail to see why it will be a problem with IO.

I think the view that it’s a non-issue comes down to familiarity via language usage. I am on the ‘everything explicit all the time’ team and see no issues with Allocator, or the proposed IO mechanism. But, programmers coming from other languages, particularly those with an expectation of implicitness being a semantic and syntactic feature can not envision programming without all of the alleged time saving/ergonomic ‘benefits’.

I have had multiple ‘arguments’ about the reasoning advantages, complete lack of time loss (over any useful timeframe for comparison), and long-term maintenance benefits of explicitness in language design. I have never convinced a single ‘implicit team’ dev that I’m right. Oh well, I will keep doing what I do and be fine and will support in whatever ways I can languages and language development that prioritizes explicitness.

Re: Zig's new plan for asynchronous programs

#185

Earlier quoted context omitted.

Yes.

Can you explain for those of us less familiar with Haskell (and monads in general)?

A reader is just an interface that allows you to build up a computation that will eventually take an environment as a parameter and return a value.

Here's the magic:

    newtype Reader env a = Reader { runReader :: env -> a }
    
    ask = Reader $ \x -> x
    
    instance Functor (Reader env) where
      fmap f (Reader g) = Reader $ \x -> f (g x)
    
    instance Applicative (Reader env) where
      pure x = Reader (\_ -> x)
      ff  fx = Reader $ \x -> (runReader ff x) (runReader fx x)
    
    instance Monad (Reader env) where
      (Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x
That Monad instance might be the scariest bit if you're unfamiliar with Haskell. The (>>=) function takes a Monad (here a Reader) and a continuation to call on it's contents. It then threads the environment through both.

Might be used like this:

    calc :: Reader String Int
    calc = do
      input 
Not sure how this compares to Zig!

https://stackoverflow.com/questions/14178889/what-is-the-pur...

Edit: Added Applicative instance so code runs on modern Haskell. Please critique! Also added example.

Re: Zig's new plan for asynchronous programs

#186

I think this design is very reasonable. However, I find Zig's explanation of it pretty confusing: they've taken pains to emphasize that it solves the function coloring problem, which it doesn't: it pushes I/O into an effect type, which essentially behaves as a token that callers need to retain. This is a form of coloring, albeit one that's much more ergonomic. (To my understanding this is pretty similar to how Go sol…

Having used zig a bit as a hobby. Why is it more ergonomic? Using await vs passing a token have similar ergonomics to me. The one thing you could say is that using some kind of token makes it dead simple to have different tokens. But that's really not something I run into often at all when using async.

Making it dead simple to have different tokens is exactly the goal. A smattering of examples recently on my mind:

As a background, you might ask why you need different runtimes ever. Why not just make everything async and be done with it, especially if the language is able to hide that complexity?

1. In the context of a systems language that's not an option. You might be writing an OS, embedded code, a game with atypical performance demands requiring more care with the IO, some kernel-bypass shenanigan, etc. Even just selecting between a few builtin choices (like single-threaded async vs multi-threaded async vs single-threaded sync) doesn't provide enough flexibility for the range of programs you're trying to allow a user to write.

2. Similarly, even initializing a truly arbitrary IO effect once at compile-time doesn't always suffice. Maybe you normally want a multi-threaded solution but need more care with respect to concurrency in some critical section and need to swap in a different IO. Maybe you normally get to interact with the normal internet but have a mode/section/interface/etc where you need to send messages through stranger networking conditions (20s ping, 99% packet loss, 0.1kbps upload on the far side, custom hardware, etc). Maybe some part of your application needs bounded latency and is fine dropping packets but some other part needs high throughput and no dropped packets at any latency cost. Maybe your disk hardware is such that it makes sense for networking to be async and disk to be sync. And so on. You can potentially work around that in a world with a single IO implementation if you can hack around it with different compilation units or something, but it gets complicated.

Part of the answer then is that you need (or really want) something equivalent to different IO runtimes, hot-swappable for each function call. I gave some high-level ideas as to why that might be the case, but high-level observations often don't resonate, so let's look at a concrete case where `await` is less ergonomic:

1. Take something like TLS as an example (stdlib or 3rd-party, doesn't really matter). The handshake code is complicated, so a normal implementation calls into an IO abstraction layer and physically does reads and writes (as opposed to, e.g., a pure state-machine implementation which returns some metadata about which action to perform next -- I hacked together a terrible version of that at one point [0] if you want to see what I mean). What if you want to run it on an embedded device? If it were written with async it would likely have enough other baggage that it wouldn't fit or otherwise wouldn't work. What if you want to hide your transmission in other data to sneak it past prying eyes (steganography, nowadays that's relatively easy to do via LLMs interestingly enough, and you can embed arbitrary data in messages which are human-readable and purport to discuss completely other things without exposing hi/lo-bit patterns or other such things that normally break steganography)? Then the kernel socket abstraction doesn't work at all, and "just using await" doesn't fix the problem. Basically, any place you want to use that library (and, arguably, that's the sort of code where you should absolutely use a library rather than rolling it yourself), if the implementer had a "just use await" mentality then you're SOL if you need to use it in literally any other context.

I was going to write more concrete cases, but this comment is getting to be too long. The general observation is that "just use await" hinders code re-use. If you're writing code for your own consumption and also never need those other uses then it's a non-issue, but with a clever choice of abstraction it _might_ be possible (old Zig had a solution that didn't quite hit the mark IMO, and time will tell if this one is good enough, but I'm optimistic) to enable the IO code people naturally write to be appropriately generic by default and thus empower future developers via a more composable set of primitives.

They really nailed that with the allocator interface, and if this works then my only real concern is a generic "what next" -- it's pushing toward an effect system, but integrating those with a systems language is mostly an unsolved problem, and adding a 3rd, 4th, etc explicit parameter to nearly every function is going to get unwieldy in a hurry (back-of-the-envelope idea I've had stewing if I ever write a whole "major" language is to basically do what Zig currently does and pack all those "effects" into a single effect parameter that you pass into each function, still allowing you to customize each function call, still allowing you to inspect which functions require allocators or whatever, but making the experience more pleasant if you have a little syntactic sugar around sub-effects and if the parent type class is comptime-known).

[0] https://github.com/hmusgrave/rayloop/blob/d5e797967c42b9c891...

Re: Zig's new plan for asynchronous programs

#187
https://danieltan.weblog.lol/2025/08/function-colors-represe...

the core problem is that language/library authors need to provide some way to bridge between different execution contexts, like containing these different contexts (sync / async) under FSMs and then providing some sort of communication channel between both.

Re: Zig's new plan for asynchronous programs

#189

I think this design is very reasonable. However, I find Zig's explanation of it pretty confusing: they've taken pains to emphasize that it solves the function coloring problem, which it doesn't: it pushes I/O into an effect type, which essentially behaves as a token that callers need to retain. This is a form of coloring, albeit one that's much more ergonomic. (To my understanding this is pretty similar to how Go sol…

This solves a problem for library authors which is that blocking and event-based io implementations of functionality look the same but are not actually the same so users end up complaining when you do one but not the other. It adds a problem of needing to pass the global kind of io through a program. I think this mostly isn’t a huge problem because typical good program design has io on the periphery and so you don’t…

There is nothing special about the [IO a] -> IO [a] in Haskell. You can iterate over it using the "normal" methods of iterating just fine.

    forM ios $ \io -> io
But there are better ways to do it (e.g. sequence), but those are also not "special" to IO in any way. They are common abstractions usable by any Monad.

Re: Zig's new plan for asynchronous programs

#190
post #106
post #73

Earlier quoted context omitted.

The coloring is not the concrete argument (Io implementation) that is passed, but whether the function has an Io parameter in the first place. Whether the implementation of a function performs IO is in principle an implementation detail that can change in the future. A function that doesn't take an Io argument but wants to call another function that requires an Io argument can't. So you end up adding Io parameters ju…

> Whether the implementation of a function performs IO is in principle an implementation detail that can change in the future. I think that's where your perspective differs from Zig developers. Performing IO, in my opinion, is categorically not an implementation detail. In the same way that heap allocation is not an implementation detail in idiomatic Zig. I don't want to find out my math library is caching results on…

> I don't want to find out my math library is caching results on disk, or allocating megabytes to memoize. I want to know what functions I can use in a freestanding environment, or somewhere resource constrained.

On that vein, I would often like to know whether the function I can is creating a task/thread/greenlet/whatever that will continue executing, concurrently, after it returns. Making that be part of the signature is approximately called “structured concurrency”, and Zig’s design seems to conflate that with taking an io parameter. This seems a bit disappointing to me.

Post reply on HN