Live data from Hacker News

Zig's new plan for asynchronous programs

lwn.net

111–120 of 274 posts

Re: Zig's new plan for asynchronous programs

#111
post #104
post #92

Earlier quoted context omitted.

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.

Maybe I have this wrong, but I believe the difference is that you can create an Io instance in a function that has none

In Rust, you can always create a new tokio runtime and use that to call an async function from a sync function. Ditto with Python: just create a new asyncio event loop and call `run`. That's actually exactly what an Io object in Zig is, but with a new name.

Looking back at the original function coloring post [1], it says:

> It is better. I will take async-await over bare callbacks or futures any day of the week. But we’re lying to ourselves if we think all of our troubles are gone. As soon as you start trying to write higher-order functions, or reuse code, you’re right back to realizing color is still there, bleeding all over your codebase.

So if this is isomorphic to async/await, it does not "solve" the coloring problem as originally stated, but I'm starting to think it's not much of a problem at all. Some functions just have different signatures from other functions. It was only a huge problem for JavaScript because the ecosystem at large decided to change the type signatures of some giant portion of all functions at once, migrating from callbacks to async.

[1]: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...

Re: Zig's new plan for asynchronous programs

#112
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.

In the JS example, a synchronous function cannot poll the result of a Promise. This is meaningfully different when implementing loops and streams. Ex, game loop, an animation frame, polling a stream.

A great example is React Suspense. To suspend a component, the render function throws a Promise. To trigger a parent Error Boundary, the render function throws an error. To resume a component, the render function returns a result. React never made the suspense API public because it's a footgun.

If a JS Promise were inspectable, a synchronous render function could poll its result, and suspended components would not need to use throw to try and extend the language.

Re: Zig's new plan for asynchronous programs

#113
post #103
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…

> A function that doesn't take an Io argument but wants to call another function that requires an Io argument can't. Why? Can’t you just create an instance of an Io of whatever flavor you prefer and use that? Or keep one around for use repeatedly? The whole “hide a global event loop behind language syntax” is an example of a leaky abstraction which is also restrictive. The approach here is explicit and doesn’t bind f…

You can, but then you’re denying your callers control over the Io. It’s not really different with async function coloring: https://news.ycombinator.com/item?id=46126310

Scheduling of IO operations isn’t hidden global state. Or if it is, then so is thread scheduling by the OS.

Re: Zig's new plan for asynchronous programs

#114
post #18

Earlier quoted context omitted.

> Every struct or bare fn now needs (2) fields/parameters by default. Storing interfaces a field in structs is becoming a bit of an an anti-pattern in Zig. There are still use cases for it, but you should think twice about it being your go-to strategy. There's been a recent shift in the standard library toward "unmanaged" containers, which don't store a copy of the Allocator interface, and instead Allocators are pass…

I’m not sure I see how each example improves on the previous (though granted, I don’t really know Zig). What happens if you call append() with two different allocators? Or if you deinit() with a different allocator than the one that actually handled the memory?

Storing an Allocator alongside the container is an additional 16-bytes. This isn't much, but starts adding up when you start storing other objects that keep allocators inside of those containers. This can improve cache locality.

It also helps devirtualization, as the most common case is threading a single allocator through your application (with the occasion Arena allocator wrapping it for grouped allocations). When the Allocator interface is stored in the container, it's harder for the optimizer to prove it hasn't changed.

> What happens if you call append() with two different allocators? Or if you deinit() with a different allocator than the one that actually handled the memory?

It's undefined behaviour, but I've never seen it be an issue in practice. Expanding on what I mentioned above, it's typical for only a single allocator to be used for long live objects throughout the entire program. Arena allocators are used for grouped allocations, and tend to have a well defined scope, so it's obvious where deallocation occurs. FixedBufferAllocator also tends to be used in the same limited scope.

Re: Zig's new plan for asynchronous programs

#115
post #92

Earlier quoted context omitted.

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.

In the JS example, a synchronous function cannot poll the result of a Promise. This is meaningfully different when implementing loops and streams. Ex, game loop, an animation frame, polling a stream. A great example is React Suspense. To suspend a component, the render function throws a Promise. To trigger a parent Error Boundary, the render function throws an error. To resume a component, the render function returns…

I see. I guess JS is the only language with the coloring problem, then, which is strange because it's one of the few with a built-in event loop.

This Io business is isomorphic to async/await in Rust or Python [1]. Go also has a built-in "event loop"-type thing, but decidedly does not have a coloring problem. I can't think of any languages besides JS that do.

[1]: https://news.ycombinator.com/item?id=46126310

Re: Zig's new plan for asynchronous programs

#117

Overall this article is accurate and well-researched. Thanks to Daroc Alden for due diligence. Here are a couple of minor corrections: > When using an Io.Threaded instance, the async() function doesn't actually do anything asynchronously — it just runs the provided function right away. While this is a legal implementation strategy, this is not what std.Io.Threaded does. By default, it will use a configurably sized th…

Hey Andrew, question for you about something the article litely touches on but doesn't really discuss further: > If the programmer uses async() where they should have used asyncConcurrent(), that is a bug. Zig's new model does not (and cannot) prevent programmers from writing incorrect code, so there are still some subtleties to keep in mind when adapting existing Zig code to use the new interface. What class of bug…

A deadlock.

For example, the function is called immediately, rather than being run in a separate thread, causing it to block forever on accept(), because the connect() is after the call to async().

If concurrent() is used instead, the I/O implementation will spawn a new thread for the function, so that the accept() is handled by the new thread, or it will return error.ConcurrencyUnavailable.

async() is infallible. concurrent() is fallible.

Re: Zig's new plan for asynchronous programs

#118
post #73

Earlier quoted context omitted.

In Zig's case you pass the argument whether or not it's asynchronous, though. The caller controls the behavior, not the function being called.

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…

[deleted]

Re: Zig's new plan for asynchronous programs

#119
post #77

Earlier quoted context omitted.

I agree. But it should be done with a proper effect system, not a pile of ad hoc hacks built on abuse of the type system.

`async` is in the type system. In your mind, how would you mark and bubble up panicky functions, etc.? What would that look like? I felt like a `panic` label for functions would be nice, but if we start stacking labels it becomes cumbersome: pub async panic alloc fn foo() {} That feels dense. I think ideally it would be something readers could spot at first glance, not something inferred.

>`async` is in the type system.

No, it's not. `async` is just syntax sugar, the "effect" gets emulated in the type system using `Future`. This is one of the reasons why the `async` system feels so foreign and requires so many language changes to make it remotely usable. `const` is much closer to a "true" effect (well, to be precise it's an anti-effect, but it's not important right now).

Also, I think it's useful to distinguish between effect and type systems, instead of lumping them into just "type system". The former applies to code and the latter to data.

>That feels dense.

Yes. But why `async` is more important than `alloc`? For some applications it's as important to know about potential allocations, as for other applications to know about whether code potentially yields or not.

Explicitly listing all effects would the most straightforward approach, but I think a more practical approach would be to have a list of "default effects", which can be overwritten on the crate (or maybe even module) level. And on the function level you will be able to opt-in or opt-out from effects if needed.

>I think ideally it would be something readers could spot at first glance

Well, you either can have "dense" or explicit "at first glance" signatures.

Re: Zig's new plan for asynchronous programs

#120

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…

I wouldn't define it as Sans-IO if you take an IO argument and block/wait on reading/writing, whether that be via threads or an event loop.

Sans-IO the IO is _outside_ completely. No read/write at all.

Post reply on HN