Live data from Hacker News

Zig's new plan for asynchronous programs

lwn.net

261–270 of 274 posts

Re: Zig's new plan for asynchronous programs

#261

Earlier quoted context omitted.

The function coloring problem actually comes up when you implement the async part using stackless coroutines (e.g. in Rust) or callbacks (e.g. in Javascript). Zig's new I/O does neither of those for now, so hence why it doesn't suffer from it, but at the same time it didn't "solve" the problem, it just sidestepped it by providing an implementation that has similar features but not exactly the same tradeoffs.

It's sans-io at the language level, I like the concept. So I did a bit of research into how this works in Zig under the hood, in terms of compilation. First things first, Zig does compile async fns to a state machine: https://github.com/ziglang/zig/issues/23446 The compiler decides at compile time which color to compile the function as (potentially both). That's a neat idea, but... https://github.com/ziglang/zig/issu…

> First things first, Zig does compile async fns to a state machine: https://github.com/ziglang/zig/issues/23446

Maybe I'm missing something, but that's still a proposal, which also assumes an implementation for the other proposal you linked and that also doesn't exist yet.

For now I would refrain from commenting on non-existing functionality.

> I still think sans-io at the language level might be the future, but this isn't a complete solution.

I'm not sure what about this is really at the language level (only stackless coroutines appear to require language level support, and it's still unclear if it's really possible to implement them). However I do agree that a sans-io, or at least dependency injection for I/O is a great improvement on the library side, and it's something I'd like to see in Rust too.

Re: Zig's new plan for asynchronous programs

#262
post #92

Earlier quoted context omitted.

The function coloring problem actually comes up when you implement the async part using stackless coroutines (e.g. in Rust) or callbacks (e.g. in Javascript). Zig's new I/O does neither of those for now, so hence why it doesn't suffer from it, but at the same time it didn't "solve" the problem, it just sidestepped it by providing an implementation that has similar features but not exactly the same tradeoffs.

How are the tradeoffs meaningfully different? Imagine that, instead of passing an `Io` object around, you just had to add an `async` keyword to the function, and that was simply syntactic sugar for an implied `Io` argument, and you could use an `await` keyword as syntactic sugar to pass whatever `Io` object the caller has to the callee. I don't see how that's not the exact same situation.

You can create `Io` instances whenever you want (although that kinda goes against its spirit) and you can also pass them inside structs, not necessarily as a function argument. Moreover you can reuse all the existing "sync" functions when using I/O and viceversa (ever tried doing an async call inside a `Option::map` in Rust?)

By the way, Rust's runtime also have a similar issue to Zig's `Io` (making the runtime available to the code, similarly to how you need to make an `Io` instance available in Zig). Rust runtimes just decided to use thread locals for that, and nothing stops you from doing the same in Zig if you want to.

I hope you can see that this is all orthogonal to the "colored functions" problem. When I was talking about tradeoffs however I was referring to the use of a threaded/green thread implementation under the hood as opposed to a stackless coroutine. The first two are less invasive at the language level and don't require function coloring (hence why Zig didn't solve, but also doesn't have, function coloring!) however they can be more limiting (they are not always available, especially on embedded and on wasm) and less extensible (most operations need to be explicitly supported in `Io`, as opposed to being implementable by anyone).

Re: Zig's new plan for asynchronous programs

#263

Earlier quoted context omitted.

> Zig actually also had solved the coloring problem in the old and abandondend async-await solution because the compiler simply stamped out a sync- or async-version of the same function based on the calling context (this works because everything is a single compilation unit). AFAIK this still leaked through function pointers, which were still sync or async (and this was not visible in their type)

Pretty sure the Zig team is aware of this and has plans to fix it before they re-release async.

I’m pretty sure that was an issue specifically with the old implementation, and not something still left to fix.

Re: Zig's new plan for asynchronous programs

#264

Earlier quoted context omitted.

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.

Sure, but that’s a library limitation (no widespread common runtime interface that libraries such as Tokio implement), not a fundamental limitation of async.

Thread safety is also a lot easier to achieve in languages like C#, and then of course you have single-threaded environments like JS and Python.

Re: Zig's new plan for asynchronous programs

#265

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.

Look at Go or Java virtual threads. Async I/O doesn't need function coloring.

Here is an example Zig code:

    defer stream.close(io);

    var read_buffer: [1024]u8 = undefined;
    var reader = stream.reader(io, &read_buffer);

    var write_buffer: [1024]u8 = undefined;
    var writer = stream.writer(io, &write_buffer);

    while (true) {
        const line = reader.interface.takeDelimiterInclusive('\n') catch |err| switch (err) {
            error.EndOfStream => break,
            else => return err,
        };
        try writer.interface.writeAll(line);
        try writer.interface.flush();
    }
The actual loop using reader/writer isn't aware of being used in async context at all. It can even live in a different library and it will work just fine.

Re: Zig's new plan for asynchronous programs

#266
post #224

Isn't this (their async version of Io) essentially the same thing that Go is doing? I seem to recall reading about some downsides to that approach, e.g. that calling C libraries is relatively expensive (because a real stack has to be allocated) and that circumventing libc to do direct syscalls is fragile and unsupported on some platforms. Does the Zig implementation improve on Go's approach? Is it just that it makes…

The goal of of the interface is to support multiple modes of operations. You can have the same code, even the same compiled binary, and they can both with either threaded/blocking functions, or stackful coroutines and event loops.

Re: Zig's new plan for asynchronous programs

#267
post #225

Earlier quoted context omitted.

It's not a monad because it doesn't return a description of how to carry out I/O that is performed by a separate system; it does the I/O inside the function before returning. That's a regular old interface, not a monad.

> 1. a description of how to carry out I/O that is performed by a separate system > 2. does the I/O inside the function before returning How do you distinguish those two things? To put my cards on the table, I believe Haskell does 2, and I think my Haskell effect system Bluefin makes this abundantly clear. (Zig's `Io` seems to correspond to Bluefin's `IOE`.) There is a persistent myth in the Haskell world (and beyond…

> I would say that "I/O is done inside `foo` before returning".

It is not. The documentation and the type very clearly shows this:

https://hackage.haskell.org/package/base-4.21.0.0/docs/Prelu...

> A value of type `IO a` is a computation which, when performed, does some I/O before returning a value of type a.

So your function foo does no IO in itself. It returns a "computation" for main to perform. And only main can do this, since the runtime calls main. You can call foo as much as you like, but nothing will be printed until you bind any of the returned IO values.

Comparing it to other languages is a bit misleading since Haskell is lazy. putStrLn isn't even evaluated until the IO value is needed. So even "before returning" is wrong no matter how you choose to define "inside".

Re: Zig's new plan for asynchronous programs

#268
post #244
post #225

Earlier quoted context omitted.

> 1. a description of how to carry out I/O that is performed by a separate system > 2. does the I/O inside the function before returning How do you distinguish those two things? To put my cards on the table, I believe Haskell does 2, and I think my Haskell effect system Bluefin makes this abundantly clear. (Zig's `Io` seems to correspond to Bluefin's `IOE`.) There is a persistent myth in the Haskell world (and beyond…

I'm also pretty sure that its immaterial if Haskell does 1 or not. This is an implementation detail and not at all important to something being a Monad or not. My understanding is requiring 1 essentially forces you to think of every Monad as being free.

Ah! My favourite Haskell discussion. So, consider these two programs, the first in Haskell:

    main :: IO ()
    main = do
      foo
      foo

    foo :: IO ()
    foo = putStrLn "Hello"
and the second in Python:

    def main():
      foo()
      foo()

    def foo():
      print("Hello")
For the Python one I'd say "I/O is done inside `foo` before returning". Would you? If not, why not? And if so, what purpose does it serve to not say the same for the Haskell?

Re: Zig's new plan for asynchronous programs

#269
post #258

Earlier quoted context omitted.

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

That makes sense. I don't know anything about embedded programming really but I thought that it really fundamentally requires async (in the conceptual sense). So you have to structure your program as an event loop no matter what. Wasn't the alleged goal of rust async to be zero-cost in the sense that the program transformation of a future ends up being roughly what you would write by hand if you have to hand-roll a state machine? Of course the runtime itself requires a runtime and I get why something like Tokio would be a non-started in embedded environments, but you can still hand-roll the core runtime and structure the rest of the code with async/await right? Or are you saying that the generated code even without the runtime is too heavy for an embedded environment?

Re: Zig's new plan for asynchronous programs

#270
post #268
post #244

Earlier quoted context omitted.

I'm also pretty sure that its immaterial if Haskell does 1 or not. This is an implementation detail and not at all important to something being a Monad or not. My understanding is requiring 1 essentially forces you to think of every Monad as being free.

Ah! My favourite Haskell discussion. So, consider these two programs, the first in Haskell: main :: IO () main = do foo foo foo :: IO () foo = putStrLn "Hello" and the second in Python: def main(): foo() foo() def foo(): print("Hello") For the Python one I'd say "I/O is done inside `foo` before returning". Would you? If not, why not? And if so, what purpose does it serve to not say the same for the Haskell?

My Haskell is rusty enough that I don’t know the proper syntax for it, but you can make a program that calls foo and then throws away / never uses the IO computation. Because Haskell is lazy, “Hello” will never be printed.
Post reply on HN