Live data from Hacker News

Zig's new plan for asynchronous programs

lwn.net

251–260 of 274 posts

Re: Zig's new plan for asynchronous programs

#251
post #249

Earlier quoted context omitted.

Are you saying "monad" is a synonym of "interface"?

Not a synonym, but `Monad` is one of the commonly used interfaces in Haskell (not the only one).

OK I think I understand now, thank you. My takeaways:

1. Yes, Zig is doing basically the same thing as Haskell

2. No, it's not a monad in Zig because it's an imperative language.

Re: Zig's new plan for asynchronous programs

#252

Earlier quoted context omitted.

My opinion is that if your library or function is doing IO, it should be async - there is no reason to support "sync I/O". Also, this "sans IO" trend is interesting, but the code boils down to a less ergonomic, more verbose, and less efficient version of async (in Rust). It's async/await with more steps, and I would argue those steps are not great.

> there is no reason to support "sync I/O" I disagree strongly. From a performance perspective, asynchronous IO makes a lot of sense when you're dealing concurrently with a large number of tasks which each spend most of their time waiting for IO operations to complete. In this case, running those tasks in a single-threaded event loop is far more efficient than launching off thousands of individual threads. However, i…

The beautiful thing about the “async” abstraction is that it doesn’t actually tie you to an event loop at all. Nothing about it implies that somebody is calling `epoll_wait` or similar anywhere in the stack.

It’s just a compiler feature that turns functions into state machines. It’s totally valid to have an async runtime that moves a task to a thread and blocks whenever it does I/O.

I do agree that async without memory safety and thread safety is a nightmare (just like all state machines are under those circumstances). Thankfully, we have languages now that all but completely solve those issues.

Re: Zig's new plan for asynchronous programs

#253
post #106

Earlier quoted context omitted.

> 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…

This is also why function coloring is not a problem, and is in fact desirable a lot of the time.

[deleted]

Re: Zig's new plan for asynchronous programs

#254
post #198

Earlier quoted context omitted.

Passing an interface as a parameter is a monad. (Io -> _) is an instance of Monad in Haskell. Haskell just has syntax to make using (any) monad much nicer. In this case, it let's you elide the `Io` parameter in the syntax if you are just going to be passing the same Io to a bunch of other functions. But it still is there.

Couldn't have said it better myself. But IIUC Andrew stated that its not a monad because it does not build up a computation and then run. Rather, its as if every function runs a `runIO#` or `runReader` every time the io parameter is used.

Monads do not need to build up a computation. The identity functor is a monad.

Re: Zig's new plan for asynchronous programs

#255
post #249

Earlier quoted context omitted.

Not a synonym, but `Monad` is one of the commonly used interfaces in Haskell (not the only one).

OK I think I understand now, thank you. My takeaways: 1. Yes, Zig is doing basically the same thing as Haskell 2. No, it's not a monad in Zig because it's an imperative language.

It still is a monad. It's just Zig doesn't have language support for monads, so it's less ergonomic.

Just as modular addition over ints in Zig forms a group, even if Zig has no notion of groups. It's just a property of the construct.

Laziness has nothing to do with it.

What that means practically for Zig, I'm unsure.

Re: Zig's new plan for asynchronous programs

#256

Earlier quoted context omitted.

My understanding of this design is that you can write the logic separately from the decision to "do I/O immediately" versus "tell me when the I/O is done" You can write a parser thats outputs a DOM and run it on a stream, or write a parser with a streaming API and run it synchronously on a buffer. You should pick the optimal tool for the situation, but there is no path dependence anymore.

Honestly I don't see how that is different than how it works in Rust. Synchronous code is a proper subset of asynchronous code. If you have a streaming API then you can have an implementation that works in a synchronous way with no overhead if you want. For example, if you already have the whole buffer in memory sometimes then you can just use it and the stream will work exactly like a loop that you would write in th…

serde is a pull parser and it would take significant modification to convert it into an incremental push parser without blocking a thread.

Re: Zig's new plan for asynchronous programs

#257
post #106

Earlier quoted context omitted.

> 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…

This is also why function coloring is not a problem, and is in fact desirable a lot of the time.

I think talking about colouring often misses the point. Sync & async code are fundamentally different; languages without coloured functions make everything async. Everything in go (for instance) is running in an async runtime, and it's all preemptable.

Re: Zig's new plan for asynchronous programs

#258
post #186

Earlier quoted context omitted.

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 atyp…

> If it were written with async it would likely have enough other baggage that it wouldn't fit or otherwise wouldn't work I'm unclear what this means. What is the other baggage in this context?

In context (embedded programming, which in retrospect is still too big of a field for this comment to make sense by itself; what I meant was embedded programming on devices with very limited RAM or other such significant restrictions), "baggage" is the fact that you don't have many options when converting async high-level code into low-level machine code. The two normal things people write into their languages/compilers/whatever (the first being much more popular, and there do exist more than just these two options) are:

1. Your async/await syntax desugars to a state machine. The set of possible states might only be runtime-known (JS, Python), or it might be comptime-known (Rust, old-Zig, arguably new-Zig if you squint a bit). The concrete value representing the current state of that state machine is only runtime-known, and you have some sort of driver (often called an "event loop", but there are other abstractions) managing state transitions.

2. You restrict the capabilities of async/await to just those which you're able to statically (compile-time) analyze, and you require the driver (the "event loop") to be compile-time known so that you're able to desugar what looks like an async program to the programmer into a completely static, synchronous program.

On sufficiently resource-constrained devices, both of those are unworkable.

In the case of (1) (by far the most common approach, and the thing I had in mind when arguing that async has potential issues for embedded programming), you waste RAM/ROM on a more complicated program involving state machines, you waste RAM/ROM on the driver code, you waste RAM on the runtime-known states in those state machines, and you waste RAM on the runtime-known boxing of events you intend to run later. The same program (especially in an embedded context where programs tend to be simpler) can easily be written by a skilled developer in a way which avoids that overhead, but reaching for async/await from the start can prevent you from reaching your goals for the project. It's that RAM/ROM/CPU overhead that I'm talking about in the word "baggage."

In the case of (2), there are a couple potential flaws. One is just that not all reasonable programs can be represented that way (it's the same flaw with pure, non-unsafe Rust and with attempts to create languages which are known to terminate), so the technique might literally not work for your project. A second is that the compiler's interpretation of the particular control flow and jumps you want to execute will often differ from the high-level plan you had in mind, potentially creating more physical bytecode or other issues. Details matter in constrained environments.

Re: Zig's new plan for asynchronous programs

#259

Earlier quoted context omitted.

> there is no reason to support "sync I/O" I disagree strongly. From a performance perspective, asynchronous IO makes a lot of sense when you're dealing concurrently with a large number of tasks which each spend most of their time waiting for IO operations to complete. In this case, running those tasks in a single-threaded event loop is far more efficient than launching off thousands of individual threads. However, i…

The beautiful thing about the “async” abstraction is that it doesn’t actually tie you to an event loop at all. Nothing about it implies that somebody is calling `epoll_wait` or similar anywhere in the stack. It’s just a compiler feature that turns functions into state machines. It’s totally valid to have an async runtime that moves a task to a thread and blocks whenever it does I/O. I do agree that async without memo…

You surely must be referring to Rust, the only multithreaded language with async-await in which data races aren't possible.

Rust is lovely and all, but is a bad example for the performance side of the argument, since in practice libraries usually have to decide on an async runtime, so in practice library users have to launch that runtime (usually Tokio) to execute the library's Futures.

Re: Zig's new plan for asynchronous programs

#260

Earlier quoted context omitted.

Let's revisit the original article[1]. It was not about arguments, but about the pain of writing callbacks and even async/await compared to writing the same code in Go. It had 5 well-defined claims about languages with colored functions: 1. Every function has a color. This is true for the new zig approach: functions that deal with IO are red, functions that do not need to deal with IO are blue. 2. The way you call a…

In my opinion you must have function coloring, it's impossible to do async (in the common sense) without it. If you break it down one function has a dependency on the async execution engine, the other one doesn't, and that alone colors them. Most languages just change the way that dependency is expressed and that can have impacts on the ergonomics.

Uncoloured async is possible, but it involves making everything async. Crossing the sync/async boundary is never trivial, so languages like go just never cross it. Everything is coroutines.
Post reply on HN