[HN Gopher] Zig's New Async I/O
       ___________________________________________________________________
        
       Zig's New Async I/O
        
       Author : afirium
       Score  : 345 points
       Date   : 2025-07-12 23:03 UTC (23 hours ago)
        
 (HTM) web link (kristoff.it)
 (TXT) w3m dump (kristoff.it)
        
       | n42 wrote:
       | This is very well written, and very exciting! I especially love
       | the implications for WebAssembly -- WASI in userspace? Bring your
       | own IO? Why not both!
        
       | dminik wrote:
       | I feel that I have to point this out once again, because the
       | article goes so far as to state that:
       | 
       | > With this last improvement Zig has completely defeated function
       | coloring.
       | 
       | I disagree with this. Let's look at the 5 rules referenced in the
       | famous "What color is your function?" article referenced here.
       | 
       | > 1. Every function has a color
       | 
       | Well, you don't have async/sync/red/blue anymore, but you now
       | have IO and non-IO functions.
       | 
       | > 2. The way you call a function depends on its color.
       | 
       | Now, technically this seems to be solved, but you still need to
       | provide IO as a parameter. Non-IO functions don't need/take it.
       | 
       | It looks like a regular function call, but there's no real
       | difference.
       | 
       | > 3. You can only call a red function from within another red
       | function
       | 
       | This still applies. You can only call IO functions from within
       | other IO functions.
       | 
       | Technically you could pass in a new executor, but is that really
       | what you want? Not to mention that you can also do this in
       | languages that don't claim to solve the coloring problem.
       | 
       | > 4. Red functions are more painful to call
       | 
       | I think the spirit still applies here.
       | 
       | > 5. Some core library functions are red
       | 
       | This one is really about some things being only possible to
       | implement in the language and/or stdlib. I don't think this
       | applies to Zig, but it doesn't apply to Rust either for instance.
       | 
       | Now, I think these rules need some tweaking, but the general
       | problem behind function coloring is that of context. Your
       | function needs some context (an async executor, auth information,
       | an allocator, ...). In order to call such a function you also
       | need to provide the context. Zig hasn't really solved this.
       | 
       | That being said, I don't think Zig's implementation here is bad.
       | If anything, it does a great job at abstracting the usage from
       | the implementation. This is something Rust fails at
       | spectacularly.
       | 
       | However, the coloring problem hasn't really been defeated.
        
         | andyferris wrote:
         | I think of it this way.
         | 
         | Given an `io` you can, technically, build another one from it
         | with the same interface.
         | 
         | For example given an async IO runime, you could create an `io`
         | object that is blocking (awaits every command eagerly). That's
         | not too special - you can call sync functions from async
         | functions. (But in JavaScript you'd have trouble calling a sync
         | function that relies on `await`s inside, so that's still
         | something).
         | 
         | Another thing that is interesting is given a blocking posix I/O
         | that also allows for creating processes or threads, you could
         | build in userspace a truly asynchronous `io` object from that
         | blocking one. It wouldn't be as efficient as one based directly
         | on iouring, and it would be old school, but it would basically
         | work.
         | 
         | Going either way (changing `io` to sync or async) the caller
         | doesn't actually care. Yes the caller needs a context, but most
         | modern apps rely on some form of dependency injection. Most
         | well-factored apps would probably benefit from a more refined
         | and domain-specific "environment" (or set of platform effects,
         | perhaps to use the Roc terminology), not Zig's posix-flavoured
         | standard library `io` thing.
         | 
         | Yes rust achieves this to some extent; you can swap an async
         | runtime for another and your app might still compile and run
         | fine.
         | 
         | Overall I like this alot - I am wondering if Richard Feldmann
         | managed to convince Andrew Kelley that "platforms" are cool and
         | some ideas were borrowed from Roc?
        
           | dminik wrote:
           | > but most modern apps rely on some form of dependency
           | injection
           | 
           | Does Zig actually do anything here? If anything, this seems
           | to be anti-Zig, where everything must be explicit.
        
             | philwelch wrote:
             | Passing in your dependencies as function arguments is a
             | form of dependency injection. It is the simplest and thus
             | arguably best form of dependency injection.
        
               | almostgotcaught wrote:
               | This is like saying arithmetic is a form of calculus, the
               | simplest form. Ie it reduces the concept (DI) to a
               | meaningless tautology.
        
         | mlugg wrote:
         | The key difference to typical async function coloring is that
         | `Io` isn't something you need specifically for asynchronicity;
         | it's something which (unless you make a point to reach into
         | very low-level primitives) you will need in order to perform
         | _any_ IO, including reading a file, sleeping, getting the time,
         | etc. It 's also just a value which you can keep wherever you
         | want, rather than a special attribute/property of a function.
         | In practice, these properties solve the coloring problem:
         | 
         | * It's quite rare for a function to unexpectedly gain a
         | dependency on "doing IO" in general. In practice, most of your
         | codebase will have access to an `Io`, and only leaf functions
         | doing pure computation will not need them.
         | 
         | * If a function does start needing to do IO, it almost
         | certainly doesn't need to actually take it as a parameter. As
         | in many languages, it's typical in Zig code to have one type
         | which manages a bunch of core state, and which the whole
         | codebase has easy access to (e.g. in the Zig compiler itself,
         | this is the `Compilation` type). Because of this, despite the
         | perception, Zig code _doesn 't_ usually pass (for instance)
         | allocators explicitly all the way down the function call graph!
         | Instead, your "general purpose allocator" is available on that
         | "application state" type, so you can fetch it from essentially
         | wherever. IO will work just the same in practice. So, if you
         | discover that a code path you previously thought was pure
         | actually does need to perform IO, then you don't need to apply
         | some nasty viral change; you just grab `my_thing.io`.
         | 
         | I do agree that in principle, there's still a form of function
         | coloring going on. Arguably, our solution to the problem is
         | just to color every function async-colored (by giving most or
         | all of them access to an `Io`). But it's much like the
         | "coloring" of having to pass `Allocator`s around: it's not a
         | problem in practice, because you'll basically always have easy
         | access to one even if you didn't previously think you'd need
         | it. I think seasoned Zig developers will pretty invariably
         | agree with the statement that explicitly passing `Allocator`s
         | around really does not introduce function coloring annoyances
         | in practice, and I see no reason that `Io` would be
         | particularly different.
        
           | dminik wrote:
           | > It's quite rare for a function to unexpectedly gain a
           | dependency on ...
           | 
           | If this was true in general, the function coloring problem
           | wouldn't be talked about.
           | 
           | However, the second point is more interesting. I think
           | there's a bit of a Stockholm syndrome thing here with Zig
           | programmers and Allocator. It's likely that Zig programmers
           | won't mind passing around an extra param.
           | 
           | If anything, it would make sense to me to have IO contain an
           | allocator too. Allocation is a kind of IO too. But I guess
           | it's going to be 2 params from now on.
        
             | throwawaymaths wrote:
             | > But I guess it's going to be 2 params from now on.
             | 
             | >> So, if you discover that a code path you previously
             | thought was pure actually does need to perform IO, then you
             | don't need to apply some nasty viral change; you just grab
             | `my_thing.io
        
               | camgunz wrote:
               | Python, for example, will let you call async functions
               | inside non-async functions, you just have to set up the
               | event loop yourself. This isn't conceptually different
               | than the Io thing here.
        
               | throwawaymaths wrote:
               | except you cant "pass the same event loop in multiple
               | locations". its also not an easy lift. the zig std will
               | provide a few standard implementations which would be
               | trivial to drop in.
        
               | camgunz wrote:
               | I thought it was the exact same; an event loop in Python
               | is just whatever Io is in Zig, make it a param, get it
               | from an import and a lookup (`import asyncio; loop =
               | asyncio.get_running_loop()`). I might be misunderstanding
               | what you're saying though.
        
               | throwawaymaths wrote:
               | hm maybe. i guess ive only used python in situations
               | where it injects it into amain so i could have been
               | confused. i thought python async was a wrapper around
               | generators, and so the mechanism cant be instantiated in
               | multiple places. i stand corrected.
        
               | camgunz wrote:
               | Oh no you're right it's a pain in the ass and weird. But
               | I think it's the way because there's no good reason to
               | have more than one event loop.
               | 
               | Also... maybe it started out as generator wrappers? I
               | think I read something that said that.
        
               | Spivak wrote:
               | > no good reason to have more than one event loop
               | 
               | Per thread--once you start working in multiple threads
               | you have the choice to have one global event loop, which
               | comes at the cost of all async code being effectively
               | serialized as far as threads are concerned*, or one event
               | loop per thread.
               | 
               | * Which can be fine if your program is mostly not async
               | but you have that one stubborn library. Yay async
               | virality.
        
               | camgunz wrote:
               | I can't say there's no good reason to have per thread
               | event loops, but I think I can say if you do know of one
               | you're suffering a terrible curse. I can only imagine the
               | constraints that would force me to do this.
        
               | Spivak wrote:
               | Because you have a main application running a web server
               | and a daemon worker thread performing potentially long
               | running tasks that use async libraries and you don't want
               | to block the responsiveness of your web server. It's
               | really not that bad, at least in Python.
        
               | camgunz wrote:
               | Well, here we go I guess. Why can't you just use FastAPI?
               | Or Tornado? Isn't there also an async Flask? Isn't Django
               | also async now? What minor god have you angered to be
               | chained to a non-async framework?
        
               | Spivak wrote:
               | Of all the responses, this was perhaps my least expected
               | one when I was talking about being chained to an async
               | framework. Async isn't a replacement for threads, async
               | doesn't let you spread your work out over multiple cores
               | and doesn't give you time slicing. In Python the asyncio
               | module actually gives you a threadpool to run
               | computationally intensive work in as kind of one-offs.
               | But when you need something like background job
               | processing and want to also reap the benefits of asyncio,
               | like being able to pull multiple tasks off the queue, and
               | progress on others while a job does io, then you need an
               | event loop in the other thread. It was specifically
               | avoiding locking up FastAPI that lead me to use multiple
               | event loops in the first place.
               | 
               | You're free to spin the job worker off to another process
               | but however you swing it it's still multiple event loops
               | you deal with. But with threads you get to only load your
               | Python app into memory once.
        
               | gpderetta wrote:
               | But the asyncio event loop is not reentrant, so your faux
               | sync functions cannot be safely called from other async
               | functions. It is an extremely leaky abstraction. This is
               | not a theoretical possibility, I stumbled on the issue 15
               | minutes into my foray into asyncio (turns out that
               | jupyter notebooks use asyncio internally).
               | 
               | There are ways around that (spawn a separate thread with
               | a dedicated event loop, then block), or monkey patch
               | asyncio, but they are all ugly.
        
             | Gibbon1 wrote:
             | I do something like that with event driven firmware. There
             | is an allocator as part of the context. And the idea that
             | the function is executing under some context seems fine to
             | me.
        
             | laserbeam wrote:
             | > If anything, it would make sense to me to have IO contain
             | an allocator too. Allocation is a kind of IO too.
             | 
             | Io in zig is for "things that can block execution". Things
             | that could semantically cause a yield of any kind.
             | Allocation is not one of those things.
             | 
             | Also, it's perfectly reasonable and sometimes desireable to
             | have 13 different allocators in your program at once. Short
             | lived ones, long lived ones, temporary allocations, super
             | specific allocators to optimize some areas of your game...
             | 
             | There are fewer reasons to want 2 different strategies to
             | handle concurrency at the same time in your program as they
             | could end up deadlocking on each other. Sure, you may want
             | one in debug builds, another in release, another when
             | running tests, but there are much fewer usecases of them
             | running side by side.
        
               | chrisohara wrote:
               | > Io in zig is for "things that can block execution".
               | Things that could semantically cause a yield of any kind.
               | Allocation is not one of those things.
               | 
               | The allocator may yield to the OS when requesting or
               | releasing memory (e.g. sbrk, mmap, munmap)?
        
               | messe wrote:
               | I don't find that a particularly compelling argument in
               | this case, because so can accessing any memory address if
               | it's not currently swapped in.
        
               | laserbeam wrote:
               | Yielding in this context means to a different "thread" in
               | your context, not the OS. If you want to express "this is
               | a point where the program can do something else" it is a
               | yield. If you block and can't switch to something else...
               | it is not.
               | 
               | So if you're using an API like mmap like that you should
               | think of it as IO (I don't think you can, but am not
               | sure).
        
               | throwawaymaths wrote:
               | the page allocator which is the root of many allocators
               | calls mmap. of course the fixed buffer allocator does
               | not.
        
           | ginko wrote:
           | > It's quite rare for a function to unexpectedly gain a
           | dependency on "doing IO" in general.
           | 
           | From the code sample it looks like printing to stdio will now
           | require an Io param. So won't you now have to pass that down
           | to wherever you want to do a quick debug printf?
        
             | dwattttt wrote:
             | I'm not familiar with Zig, but won't the classic "blocking"
             | APIs still be around? I'd rather a synchronous debug print
             | either way.
        
               | laserbeam wrote:
               | Yes. You can always use the blocking syscalls your OS
               | provides and ignore the Io system for stuff like that. No
               | idea how they'd do that by default in the stdlib, but it
               | will definitely be possible.
        
             | throwawaymaths wrote:
             | std.debug.print(..) prints to stderr whuch does not need an
             | io param.
        
             | flohofwoe wrote:
             | Zig has specifically a std.debug.print() function for debug
             | printing and a std.log module for logging. Those don't
             | necessarily need to be wired up with the whole stdio
             | machinery.
        
             | osigurdson wrote:
             | I think the key is, if you don't have an "io" in your call
             | stack you can always create one. At least I hope that is
             | how it would work. Otherwise it is equally viral to async
             | await.
        
           | FlyingSnake wrote:
           | > Arguably, our solution to the problem is just to color
           | every function async-colored.
           | 
           | This is essentially how Golang achived color-blindness.
        
             | cogman10 wrote:
             | It's basically how Java does it (circa 17) as well.
             | 
             | It's something you really can't do without a pretty
             | significant language runtime. You also really need people
             | working within your runtime to prefer being in your
             | runtime. Environments that do a lot of FFI don't work well
             | with a colorblind runtime. That's because if the little C
             | library you call does IO then you've got an incongruous
             | interaction that you need to worry about.
        
               | gpderetta wrote:
               | Neither Java nor Go make all functions async. What they
               | provide is stackful coroutines (or equivalently one shot
               | continuations) that allow composing async and sync
               | functions transparently.
        
           | SkiFire13 wrote:
           | > I do agree that in principle, there's still a form of
           | function coloring going on. Arguably, our solution to the
           | problem is just to color every function async-colored
           | 
           | I feel like there are two issues with this approach:
           | 
           | - you basically rely on the compiler/stdlib to silently
           | switch the async implementation, effectively implementing a
           | sort of hidden control flow which IMO doesn't really fit Zig
           | 
           | - this only solves the "visible" coloring issue of async vs
           | non-async functions, but does not try to handle the issue of
           | blocking vs non-blocking functions, rather it hides it by
           | making all functions have the same color
           | 
           | - you're limiting the set of async operations to the ones
           | supported in the `Io`'s vtable. This forces it to e.g.
           | include mutexes, even though they are not really I/O, because
           | they might block and hence need async support. But if I wrote
           | my own channel how would this design support it?
        
           | littlestymaar wrote:
           | > * It's quite rare for a function to unexpectedly gain a
           | dependency on "doing IO" in general.
           | 
           | I don't know where you got this, but it's definitely not the
           | case, otherwise async would never cause problems either. (Now
           | the problem in both cases is pretty minor, you just need to
           | change the type signature of the call stack, which isn't
           | generally that big, but it's exactly the same situation)
           | 
           | > In practice, most of your codebase will have access to an
           | `Io`, and only leaf functions doing pure computation will not
           | need them.
           | 
           | So it's exactly similar to making all of your functions
           | _async_ by default...
        
           | immibis wrote:
           | Colouring every function async-coloured by default is
           | something that's been attempted in the past; it was called
           | "threads".
           | 
           | The innovation of async over threads is simply to allocate
           | call stack frames on the heap, in linked lists or linked DAGs
           | instead of fixed-size chunks. This sounds inefficient, and it
           | is: indexing a fixed block of memory is much cheaper. It
           | comes with many advantages as well: each "thread" only
           | occupies the amount of memory it actually uses, so you can
           | have a lot more of them; you can have non-linear graphs, like
           | one function that calls two functions at the same time; and
           | by reinventing threading from scratch you avoid a lot of
           | thread-local overhead in libraries because they don't know
           | about your new kind of threads yet. Because it's inefficient
           | (and because for some reason we run the new threading system
           | on top of the old threading system), it also became useful to
           | run CPU-bound functions in the old kind of stack.
           | 
           | If you keep the linked heap activation records but don't
           | colour functions, you might end up with Go, which already
           | does this. Go can handle a large number of goroutines because
           | they use linked activation records (but in chunks, so that
           | not every function call allocates) and every Go function uses
           | them so there is no colour.
           | 
           | You do lose advantages that are specific to coloured async -
           | knowing that a context switch will not occur inside certain
           | function calls.
           | 
           | As usual, we're beating rock with paper in the moment,
           | declaring that paper is clearly the superior strategy, and
           | missing the bigger picture.
        
         | n42 wrote:
         | Aside from the ridiculous argument that function parameters
         | color them, the assertion that you can't call a function that
         | takes IO from inside a function that does not is false, since
         | you can initialize one to pass it in
        
           | dminik wrote:
           | To me, there's no difference between the IO param and
           | async/await. Adding either one causes it to not be callable
           | from certain places.
           | 
           | As for the second thing:
           | 
           | You can do that, but... You can also do this in Rust. Yet
           | nobody would say Rust has solved function coloring.
           | 
           | Also, check this part of the article:
           | 
           | > In the less common case when a program instantiates more
           | than one Io implementation, virtual calls done through the Io
           | interface will not be de-virtualized, ...
           | 
           | Doing that is an instant performance hit. Not to mention
           | annoying to do.
        
             | n42 wrote:
             | You're allowed to not like it, but that doesn't change that
             | your argument that this is a form of coloring is
             | objectively false. I'm not sure what Rust has to do with
             | it.
        
               | rowanG077 wrote:
               | Sure it is a function coloring. Just in a different form.
               | `async` in other languages is something like an implicit
               | parameter. In zig they made this implicit parameter
               | explicit. Is that more better/more ergonomic? I don't
               | know yet. The sugar is different, but the end result the
               | same. Unless you can show me concrete example of things
               | that the approach zig has taken can do that is not
               | possible in say, rust. Than I don't buy that it's not
               | just another form of function coloring.
        
               | throwawaymaths wrote:
               | > Unless you can show me concrete example
               | 
               | add io to a struct and let the struct keep track of its
               | own io.
        
               | gfaster wrote:
               | Unless I'm misunderstanding, that's effectively
               | implementing Future for the struct
        
               | masklinn wrote:
               | It's more like adding a runtime handle to the struct.
               | 
               | Modulo that I'm not sure any langage with a sync/async
               | split has an "async" runtime built entirely out of sync
               | operations. So a library can't take a runtime for a
               | caller and get whatever implementation the caller decided
               | to use.
        
               | dwattttt wrote:
               | > I'm not sure any langage with a sync/async split has an
               | "async" runtime built entirely out of sync operations.
               | 
               | You get into hairy problems of definition, but you can
               | definitely create an "async" runtime out of "sync"
               | operations: implement an async runtime with calls to C. C
               | doesn't have a concept of "async", and more or less all
               | async runtime end up like this.
               | 
               | I've implemented Future (Rust) on a struct for a Windows
               | operation based only on C calls into the OS. The struct
               | maintains everything needed to know the state of the IO,
               | and while I coupled the impl to the runtime for
               | efficiency (I've written it too), it's not strictly
               | necessary from memory.
        
               | masklinn wrote:
               | > You get into hairy problems of definition, but you can
               | definitely create an "async" runtime out of "sync"
               | operations: implement an async runtime with calls to C. C
               | doesn't have a concept of "async", and more or less all
               | async runtime end up like this.
               | 
               | While C doesn't have async OS generally provide APIs
               | which are non-blocking, and that is what async runtimes
               | are implemented on top of.
               | 
               | By sync operations I mean implementing an "async" runtime
               | entirely atop blocking operations, without bouncing them
               | through any sort of worker threads or anything.
        
               | rowanG077 wrote:
               | You can also carry around a runtime and dispatch async
               | function in non-async functions.
        
               | dminik wrote:
               | It's funny, but I do actually like it. It's just that it
               | walks like a duck, swims like a duck and quacks like a
               | duck.
               | 
               | I don't have a problem with IO conceptually (but I do
               | have a problem with Zig ergonomics, allocator included).
               | I do have a problem with claiming you defeated function
               | coloring.
               | 
               | Like, look. You didn't even get rid of await ...
               | 
               | > try a_future.await(io);
        
               | mlugg wrote:
               | I mean... you use `await` if you've used `async`. It's
               | your choice whether or not you do; and if you don't want
               | to, your callers and callees can still freely `async` and
               | `await` if they want to. I don't understand the point
               | you're trying to make here.
               | 
               | To be clear, where many languages require you to write
               | `const x = await foo()` every time you want to call an
               | async function, in Zig that's just `const x = foo()`.
               | This is a key part of the colorless design; you can't be
               | required to acknowledge that a function is async in order
               | to use it. You'll only use `await` if you first use
               | `async` to explicitly say "I want to run this
               | asynchronously with other code here if possible". If you
               | need the result immediately, that's just a function call.
               | Either way, your caller can make its own choice to call
               | you or other functions as `async`, or not to; as can your
               | callees.
        
               | dminik wrote:
               | > in Zig that's just ...
               | 
               | Well, no. In zig that's `const x = foo(io)`.
               | 
               | The moment you take or even know about an io, your
               | function is automatically "generic" over the IO
               | interface.
               | 
               | Using stackless coroutines and green threads results in a
               | completely different codegen.
               | 
               | I just noticed this part of the article:
               | 
               | > Stackless Coroutines > > This implementation won't be
               | available immediately like the previous ones because it
               | depends on reintroducing a special function calling
               | convention and rewriting function bodies into state
               | machines that don't require an explicit stack to run. > >
               | This execution model is compatible with WASM and other
               | platforms where stack swapping is not available or
               | desireable.
               | 
               | I wonder what will happen if you try to await a future
               | created with a green thread IO using a stackless
               | coroutine IO.
        
               | mlugg wrote:
               | > Well, no. In zig that's `const x = foo(io)`.
               | 
               | If `foo` needs to do IO, sure. Or, more typically (as I
               | mentioned in a different comment), it's something like
               | `const x = something.foo()`, and `foo` can get its `Io`
               | instance from `something` (in the Zig compiler this would
               | be a `Compilation` or a `Zcu` or a `Sema` or something
               | like that).
               | 
               | > Using stackless coroutines and green threads results in
               | a completely different codegen.
               | 
               | Sure, but that's abstracted away from you. To be clear,
               | stackless coroutines are the _only_ case where the
               | codegen of callers is affected, which is why they require
               | a language feature. Even if your application uses two
               | `Io` implementations for some reason, one of which is
               | based on stackless coroutines, functions using the API
               | are not duplicated.
               | 
               | > I wonder what will happen if you try to await a future
               | created with a green thread IO using a stackless
               | coroutine IO.
               | 
               | Mixing futures from any two different `Io`
               | implementations will typically result in Illegal Behavior
               | -- just like passing a pointer allocated with one
               | `Allocator` into the `free` of a different `Allocator`
               | does. This really isn't a problem. Even with allocators,
               | it's pretty rare for people to mess this up, and with
               | allocators you often _do_ have multiple of them available
               | in one place (e.g. a gpa and an arena). In contrast, it
               | will be extraordinarily rare to have more than one `Io`
               | lying around. Even if you do mess it up, the IB will
               | probably just trip a safety check, so it shouldn 't take
               | you too long to realise what you've done.
        
               | dminik wrote:
               | I find these two statements to be contradictory
               | 
               | > Sure, but that's abstracted away from you
               | 
               | > Mixing futures from any two different `Io`
               | implementations will typically result in Illegal Behavior
               | 
               | Thinking about it more, you've possibly added even more
               | colors. Each executor adds a different color and while
               | each function is color-agnostic (but not colorless)
               | futures aren't.
               | 
               | > it will be extraordinarily rare to have more than one
               | `Io`
               | 
               | Will it? I can immediately think of a use case where a
               | program might want to block for files on disk, but defer
               | fetching from network to some background async executor.
        
               | dminik wrote:
               | Also, I do find it funny that we went from "Zig has
               | completely defeated function coloring" to "Zig has
               | colored objects".
        
               | throwawaymaths wrote:
               | but that's not even the case, because it's certainly
               | possible to write a function that receives an object that
               | holds onto an io (and uses it in its vtable calls) that
               | equally well receives an object that doesn't have
               | anything to do with io [0]. The consumers of those
               | objects don't have to care, so there's no coloring.
               | 
               | [0] and this isn't even really a theoretical matter,
               | having colorblind object passing is extremely useful for
               | say, mocking. Oh, I have a database lookup/remote API
               | call, which obviously requires io, but i want fast tests
               | and I can mock it with an object with preseeded
               | values/expects -- hey, that doesn't require IO.
        
               | n42 wrote:
               | I do want to say that I regretted that comment as
               | nonconstructive after it was too late to edit it. Others
               | in the thread are representing my argument better than I
               | can or care to.
        
             | bmurphy1976 wrote:
             | >To me, there's no difference between the IO param and
             | async/await.
             | 
             | You can't pass around "async/await" as a value attached to
             | another object. You can do that with the IO param. That is
             | _very_ different.
        
               | jolux wrote:
               | > You can't pass around "async/await" as a value attached
               | to another object
               | 
               | Sure you can? You can just pass e.g. a Task around in C#
               | without awaiting it, it's when you need a result from a
               | task that you must await it.
        
               | MrJohz wrote:
               | Sure you can. An `async` function in Javascript is
               | essentially a completely normal function that returns a
               | promise. The `async`/`await` syntax is a convenient
               | syntax sugar for working with promises, but the issue
               | would still exist if it didn't exist.
               | 
               | More to the point, the issue would still exist even if
               | promises didn't exist -- a lot of Node APIs originally
               | used callbacks and a continuation-passing style approach
               | to concurrency, and that had exactly the same issues.
        
               | dminik wrote:
               | Conceptually, there's not much of a difference.
               | 
               | If you have a sync/non-IO function that now needs to do
               | IO, it becomes async/IO. And since IO and async are
               | viral, it's callers must also now be IO/async and call it
               | with IO/await. All the way up the call stack.
        
             | throwawaymaths wrote:
             | > Adding either one causes it to not be callable from
             | certain places.
             | 
             | you can call a function that requires an io parameter from
             | a function that doesn't have one by passing in a global io
             | instance?
             | 
             | as a trivial example the fn main entrypoint in zig will
             | never take an io parameter... how do you suppose you'd
             | bootstrap the io parameter that you'd eventually need. this
             | is unlike other languages where main might or might not be
             | async.
        
               | ginko wrote:
               | >you can call a function that requires an io parameter
               | from a function that doesn't have one by passing in a
               | global io instance?
               | 
               | How will that work with code mixing different Io
               | implementations? Say a library pulled in uses a global Io
               | instance while the calling code is using another.
               | 
               | I guess this can just be shot down with "don't do that"
               | but it feels like a new kind of pitfall get get into.
        
               | throwawaymaths wrote:
               | > Say a library pulled in uses a global Io instance while
               | the calling code is using another.
               | 
               | it'll probably carry a stigma like using unsafe does.
        
               | TUSF wrote:
               | Zig already has an Allocator interface that gets passed
               | around, and the convention is that libraries don't select
               | an Allocator. Only provide APIs that accept allocators.
               | If there's a certain process that works best with an
               | Arena, then the API may wrap a provided function in an
               | Arena, but not decide on their own underlying allocator
               | for the user.
               | 
               | For Zig users, adopting this same mindset for Io is not
               | really anything new. It's just another parameter that
               | occasionally needs to be passed into an API.
        
               | kristoff_it wrote:
               | while not really idiomatic, as long as you let the user
               | define the Io instance (eg with some kind of init
               | function), then it doesn't really matter how that value
               | is accessed within the library itself.
               | 
               | that's why this isn't really the same as async "coloring"
        
               | masklinn wrote:
               | You can call an async function from a function that is
               | not async by passing in a global runtime (/ event loop).
               | 
               | As a trivial example the main entry point in rust is
               | never async. How'd you suppose you'd bootstrap the
               | runtime that you'd eventually need.
               | 
               | This is pretty much like every other langage.
        
               | almostgotcaught wrote:
               | People in software really do have poor abilities to see
               | the forest for the trees don't they lol
        
               | throwawaymaths wrote:
               | and yet... there are libraries that were written twice,
               | once for async and once for not.
        
               | masklinn wrote:
               | Which should be a pretty big hint that you've
               | misidentified the issue.
        
             | delamon wrote:
             | > Doing that is an instant performance hit. Not to mention
             | annoying to do.
             | 
             | The cost of virtual dispatch on IO path is almost always
             | negligible. It is literally one conditional vs syscall. I
             | doubt it you can even measure the difference.
        
           | throwawaymaths wrote:
           | > you can't call a function that takes IO from inside a
           | function that does not is false, since you can initialize one
           | to pass it in
           | 
           | that's not true. suppose a function foo(anytype) takes a
           | struct, and expects method bar() on the struct.
           | 
           | you could send foo() the struct type Sync whose bar() does
           | not use io. or you could send foo() the struct type Async
           | whose bar uses an io _stashed in the parameter_ , and there
           | would be no code changes.
           | 
           | if you don't prefer compile time multireification, you can
           | also use type erasure and accomplish the same thing with a
           | vtable.
        
             | n42 wrote:
             | It's hard to parse your comment, but I think we are
             | agreeing? I was refuting the point of the parent. you have
             | given another example of calling an IO-taking function
             | inside a non-IO taking function. the example I gave was
             | initializing an IO inside the non-IO taking function. you
             | could also, as pointed out elsewhere, use global state.
        
         | ayuhito wrote:
         | Go also suffers from this form of "subtle coloring".
         | 
         | If you're working with goroutines, you would always pass in a
         | context parameter to handle cancellation. Many library
         | functions also require context, which poisons the rest of your
         | functions.
         | 
         | Technically, you don't have to use context for a goroutine and
         | could stub every dependency with context.Background, but that's
         | very discouraged.
        
           | tidwall wrote:
           | Context is not required in Go and I personally encourage you
           | to avoid it. There is no shame in blazing a different path.
        
             | nu11ptr wrote:
             | What would you use in its place? I've never had an issue
             | with it. I use it for 1) early termination 2) carrying
             | custom request metadata.
             | 
             | I don't really think it is fully the coloring problem
             | because you can easily call non-context functions from
             | context functions (but not other way around, so one way
             | coloring issue), but you need to be aware the cancellation
             | chain of course stops then.
        
             | schrodinger wrote:
             | Why do you encourage avoiding it? Afaik it's the only way
             | to early-abort an operation since Goroutines operate in a
             | cooperative, not preemptive, paradigm. To be very clear,
             | I'm asking this completely in good faith looking to learn
             | something new!
        
               | pjmlp wrote:
               | You are expecting them to actually check the value, there
               | is nothing preemptive.
               | 
               | Another approach is special messages over a side channel.
        
               | deepsun wrote:
               | So instead of a context you need to pass a a channel.
               | Same problem.
        
               | pjmlp wrote:
               | Not necessarily, that is one of the reasons OOP exists.
               | 
               | Have a struct representing the set of associated
               | activities, owning the channel.
        
               | ozgrakkurt wrote:
               | soo, a context?
        
               | ncruces wrote:
               | You can take that view, yes.
               | 
               | But if you store your context in a struct (which is not
               | the recommend "best practice" - but which you can do)
               | it's no longer a function coloring issue.
               | 
               | I do that in on of my libraries and I feel that it's the
               | right call (for that library).
        
               | lenkite wrote:
               | If the struct has a well-scoped and short-lived
               | lifecycle, then it is actually better to put the context
               | in the struct. Many Go libraries including the stdlib do
               | this despite not being "best practice".
               | 
               | An exception to the short-lived rule is to put context in
               | your service struct and pass it as the base context when
               | constructing the HTTP server, so that when you get a
               | service shutdown signal, one can cancel requests
               | gracefully.
        
               | ncruces wrote:
               | It's well scoped, but not short lived; it's an SQLite
               | connection.
               | 
               | But the API surface is huge, with 100s of methods on the
               | connection and derived objects, with it being unclear
               | which might block and be worthy of asynchronous
               | cancellation. You never know when pulling an additional
               | column if _that_ one might be an overflow text /blob that
               | does additional IO.
               | 
               | The solution, while not amazing is a method that you use
               | like this:                 old := conn.SetInterrupt(ctx)
               | defer conn.SetInterrupt(old)
               | 
               | This changes the "interrupt" context for the duration of
               | your function scope, and covers all potentially blocking
               | calls that you might make. Also, from the name, it's
               | quite clear that this context is used only for
               | interruption/cancellation (interrupt is the SQLite name
               | for this, which I try to adhere to).
        
               | pjmlp wrote:
               | Nope, because I didn't mention it was to be passed around
               | as a compulsory parameter, rather have your logic
               | organised across structs with methods, hide most details
               | behind interfaces.
        
               | sapiogram wrote:
               | > Afaik it's the only way to early-abort an operation
               | since Goroutines operate in a cooperative, not
               | preemptive, paradigm.
               | 
               | I'm not sure what you mean here. Preemptive/coorporative
               | terminology refers to interrupting (not aborting) a CPU-
               | bound task, in which case goroutines are fully preemptive
               | _on most platforms_ since Go 1.14, check the release
               | notes for more info. However, this has nothing to do with
               | context.
               | 
               | If you're referring to early-aborting IO operations, then
               | yes, that's what context is for. However, this doesn't
               | really have anything to do with goroutines, you could do
               | the same if the runtime was built on OS threads.
        
               | kbolino wrote:
               | Goroutines are preemptive only to the runtime scheduler.
               | You, the application developer merely using the language,
               | cannot directly preempt a goroutine.
               | 
               | This makes goroutines effectively cooperative still from
               | the perspective of the developer. The preemptive runtime
               | "just" prevents things like user code starving out the
               | garbage collector. To interrupt a goroutine, your options
               | are generally limited to context cancelation and closing
               | the channel or socket being read, if any. And the
               | goroutine may still refuse to exit (or whatever else you
               | want it to do), though that's largely up to how you code
               | it.
               | 
               | This difference is especially stark when compared with
               | Erlang/BEAM where you can directly address, signal, and
               | terminate its lightweight processes.
        
             | atombender wrote:
             | It's not required, but eschewing it ends up going against
             | the grain, since so much of the ecosystem is written to use
             | contexts, including the standard library.
             | 
             | For example, say you instead of contexts, you use channels
             | for cancellation. You can have a goroutine like this:
             | go func() {           for {             select {
             | case <-stop:               return             case
             | <-time.After(1*time.Second):               resp :=
             | fetchURL(url)               processResult(resp.Body)  //
             | Simplified, of course             }           }         }()
             | 
             | If you want to be able to shut this goroutine down
             | gracefully, you're going to have an issue where http.Get()
             | may stall for a long time, preventing the goroutine from
             | quitting.
             | 
             | Likewise, processResult() may be doing stuff that cannot be
             | aborted just by closing the stop channel. You could pass
             | the stop channel to it, but now you're just reinventing
             | contexts.
             | 
             | Of course, you can choose to only use contexts where you're
             | forced to, and invent wrappers around standard library
             | stuff (e.g. the HTTP client), but at that point you're
             | going pretty far to avoid them.
             | 
             | I do think the context is problematic. For the purposes of
             | cancellation, it's invasive and litters the call graph with
             | parameters and variables. Goroutines really ought to have
             | an implicit context inherited from its parent, since
             | _everything_ is using it anyway.
             | 
             | Contexts are _wildly_ abused for passing data around,
             | leading to bloated contexts and situations where you can 't
             | follow the chain of data-passing without carefully
             | reviewing the entire call graph. I always recommend not
             | being extremely discriminating about where to pass values
             | in context. A core principle is that it has to be something
             | that is so pervasive that it would be egregious to pass
             | around explicitly, such as loggers and application-wide
             | feature flags.
        
           | oefrha wrote:
           | The thing about context is it can be a lot more than a
           | cancellation mechanism. You can attach anything to it--
           | metadata, database client, logger, whatever. Even Io and
           | Allocator if you want to. Signatures are future-proof as long
           | as you take a context for everything.
           | 
           | At the end of the day you have to pass _something_ for
           | cooperative multitasking.
           | 
           | Of course it's also trivial to work around if you don't like
           | the pattern, "very discouraged" or not.
        
           | zer00eyz wrote:
           | > If you're working with goroutines, you would always pass in
           | a context parameter to handle cancellation.
           | 
           | The utility of context could be called a subtle coloring. But
           | you do NOT need context at all. If your dealing with
           | data+state (around queue and bus processing) its easy to
           | throw things into a goroutine and let the chips fall where
           | they will.
           | 
           | > which poisons the rest of your functions. You are free to
           | use context dependent functions without a real context:
           | https://pkg.go.dev/context#TODO
        
           | arp242 wrote:
           | Having all async happen completely transparently is not
           | really logically possible. asynchronous logic is frequently
           | fundamentally different from synchronous logic, and you need
           | to do _something_ different one way or the other. I don 't
           | think that's really the same as "function colouring".
           | 
           | And context is used for more than just goroutines. Even a
           | completely synchronous function can (and often does) take a
           | context, and the cancellation is often useful there too.
        
           | phplovesong wrote:
           | Like you said you dont NEED context. Its just something thats
           | available if you need it. I still think Go/Erlang has one of
           | the best concurrency stories out there.
        
           | osigurdson wrote:
           | I think the main point is in something like Go, the approach
           | is non-viral. If you are 99 levels deep in synchronous code
           | and need to call something with context, well, you can just
           | create one. With C#, if you need to refactor all 99 levels
           | above (or use bad practices which is of course what everyone
           | does).
           | 
           | Also, in general cancellation is something that you want to
           | optionally have with any asynchronous function so I don't
           | think there really exists an ideal approach that doesn't
           | include it. In my opinion the approach taken by Zig looks
           | pretty good.
        
         | kristoff_it wrote:
         | Here's a trick to make every function red (or blue? I'm
         | colorblind, you decide):                   var io: std.Io =
         | undefined;              pub fn main() !void {            var
         | impl = ...;            io = impl.io();         }
         | 
         | Just put io in a global variable and you won't have to worry
         | about coloring in your application. Are your functions blue,
         | red or green now?
         | 
         | Jokes aside, I agree that there's obviously a non-zero amount
         | of friction to using the `Io` intreface, but it's something
         | qualitatively very different from what causes actual real-world
         | friction around the use of async await.
         | 
         | > but the general problem behind function coloring is that of
         | context
         | 
         | I would disagree, to me the problem seems, from a practical
         | perspective that:
         | 
         | 1. Code can't be reused because the async keyword statically
         | colors a function as red (e.g. python's blocking redis client
         | and asyncio-redis). In Zig any function that wants to do Io, be
         | it blue (non-async) or red (async) still has to take in that
         | parameter so from that perspective the Io argument is
         | irrelevant.
         | 
         | 2. Using async and await opts you automatically into stackless
         | coroutines with no way of preventing that. With this new I/O
         | system even if you decide to use a library that interally uses
         | async, you can still do blocking I/O, if you want.
         | 
         | To me these seems the real problems of function coloring.
        
           | ismailmaj wrote:
           | The global io trick would totally be valid if you're writing
           | an application (i.e. not a library) and don't have use of two
           | different implementations of io
        
             | throwawaymaths wrote:
             | you could still have a library-global io, let the user set
             | it as desired.
             | 
             | > use of two different implementations of io
             | 
             | functionally rare situation.
        
             | laserbeam wrote:
             | There are plenty of libraries out there which require users
             | to do an init() call of some sorts at startup. It is
             | perfectly possible to design a library that only works with
             | 1 io instance and gets it at init(). Whether people like or
             | want that... I have no clue.
        
           | dminik wrote:
           | Well, it's not really a joke. That's a valid strategy that
           | languages use. In Go, every function is "async". And it
           | basically blocks you from doing FFI (or at least it used
           | to?). I wonder if Zig will run into similar issues here.
           | 
           | > 1. Code can't be reused because the async keyword
           | statically colors a function
           | 
           | This is fair. And it's also a real pain point with Rust.
           | However, it's funny that the "What color is your function?"
           | article doesn't even really mention this.
           | 
           | > 2. Using async and await opts you automatically into
           | stackless coroutines with no way of preventing that
           | 
           | This however I don't think is true. Async/await is mostly
           | syntax sugar.
           | 
           | In Rust and C# it uses stackless coroutines.
           | 
           | In JS it uses callbacks.
           | 
           | There's nothing preventing you from making await suspend a
           | green thread.
        
             | kristoff_it wrote:
             | I should have specified that better, of course async and
             | await can be lowered to different things (that's what Zig
             | does afterall), what I wanted to say is that that's how it
             | works in general. JS is a good counter example, but for all
             | other mainstream languages, async means stackless
             | coroutines (python, ruby, c#, rust, ...).
             | 
             | Which means that if I want to use a dependency that uses
             | async await, it's stackless coroutines for me too whether I
             | like it or not.
        
               | sczi wrote:
               | In ruby async is based on stackful fibers. With
               | https://github.com/socketry/async-debug you can see a
               | tree of all fibers with their full call stack. It also
               | avoids the problem people talk about in this thread with
               | go of passing a context parameter everywhere for
               | cancellation as you can kill or raise any exception
               | inside another fiber. I haven't used them but PHP fibers
               | are also supposedly stackful. And Java and every JVM
               | language has them since project loom in JDK 21.
        
               | kristoff_it wrote:
               | My mistake, I should have said Python then.
        
             | thayne wrote:
             | > And it basically blocks you from doing FFI
             | 
             | It doesn't block it. But it does make FFI much more
             | expensive in go than in languages like Rust, because every
             | foreign call needs to set up a c-compatible stack.
        
         | cryptonector wrote:
         | > Well, you don't have async/sync/red/blue anymore, but you now
         | have IO and non-IO functions.
         | 
         | > However, the coloring problem hasn't really been defeated.
         | 
         | Well, yes, but if the only way to do I/O were to have an Io
         | instance to do it with then Io would infect all but pure(ish,
         | non-Io) functions, so calling Io functions would be possible in
         | all but those contexts where calling Io functions is explicitly
         | something you don't want to be possible.
         | 
         | So in a way the color problem is lessened.
         | 
         | And on top of that you get something like Haskell's IO monad
         | (ok, no monad, but an IO interface). Not too shabby, though
         | you're right of course.
         | 
         | Next Zig will want monadic interfaces so that functions only
         | have to have one special argument that can then be hidden.
        
           | throwawaymaths wrote:
           | Zig's not really about hiding things but you could put it in
           | an options struct that has defaults unless overridden at
           | compile time.
        
         | nmilo wrote:
         | The original "function colouring" blogpost has done irreparable
         | damage to PL discussions because it's such a stupid concept to
         | begin with. Of course I want async functions to be "coloured"
         | differently, they do different things! How else is a "normal
         | function" supposed to call a function that gives you a result
         | _later_ ----obviously you want to be forced to say what to do
         | with the result; await it, ignore it, .then() in JS terms, etc.
         | these are important decisions that you can't just ignore
         | because it's "painful"
        
           | yxhuvud wrote:
           | There is nothing obvious around that - it is driven by what
           | abstractions the language provides related to concurrency,
           | and with different choices you will end needing different
           | ways to interact with it.
           | 
           | So yes, given how the language designers of C# and JavaScript
           | choose to implement concurrency and the APIs around that,
           | then coloring is necessary. But it is very much
           | implementation driven and implementation of other concurrency
           | models then other ways to do it that don't involve keywords
           | can make sense. So when people complain about function
           | coloring, they are complaining about the choice of
           | concurrency model that a language uses.
        
         | throwawaymaths wrote:
         | > Technically you could pass in a new executor, but is that
         | really what you want?
         | 
         | why does it have to be new? just use one executor, set it as
         | const in some file, and use that one at every entrypoint that
         | needs io! now your io doesn't propagate downwards.
        
         | jaredklewis wrote:
         | So this is a tangent from the main article, but this comment
         | made me curious and I read the original "What color is Your
         | Function" post.
         | 
         | It was an interesting read, but I guess I came away confused
         | about why "coloring" functions is a problem. Isn't "coloring"
         | just another form of static typing? By giving the compiler (or
         | interpreter) more meta data about your code, it can help you
         | avoid mistakes. But instead of the usual "first argument is an
         | integer" type meta data, "coloring" provides useful information
         | like: "this function behaves in this special way" or "this
         | function can be called in these kinds of contexts." Seems
         | reasonable?
         | 
         | Like the author seems very perturbed that there can be
         | different "colors" of functions, but a function that merely
         | calculates (without any IO or side-effects) is different than
         | one that does perform IO. A function with only synchronous code
         | behaves very differently than one that runs code inside another
         | thread or in a different tick of the event loop. Why is it bad
         | to have functions annotated with this meta data? The functions
         | behave in a fundamentally different way whether you give them
         | special annotations/syntax or not. Shouldn't different things
         | look different?
         | 
         | He mentions 2015 era Java as being ok, but as someone that's
         | written a lot of multithreaded Java code, it's easy to mess up
         | and people spam the "synchronized" keyword/"color" everywhere
         | as a result. I don't feel the lack of colors in Java makes it
         | particularly intuitive or conceptually simpler.
        
           | com2kid wrote:
           | > Isn't "coloring" just another form of static typing?
           | 
           | Yes, and so is declaring what exceptions a function can throw
           | (checked exceptions in Java).
           | 
           | > Why is it bad to have functions annotated with this meta
           | data? The functions behave in a fundamentally different way
           | whether you give them special annotations/syntax or not.
           | Shouldn't different things look different?
           | 
           | It really isn't a problem. The article makes people think
           | they've discovered some clever gotcha when they first read
           | it, but IMHO people who sit down for a bit and think through
           | the issue come to the same conclusion you have - Function
           | coloring isn't a problem in practice.
        
             | kristoff_it wrote:
             | > but IMHO people who sit down for a bit and think through
             | the issue come to the same conclusion you have - Function
             | coloring isn't a problem in practice.
             | 
             | I dunno man, have you seen people complain about async
             | virality in Rust being annoying? Have you ever tried to
             | read a backtrace from a program that does stackless
             | coroutines (it's not fun)? Have you seen people do
             | basically duplicate work to maintain a blocking and an
             | async version of the same networking library?
        
               | pimeys wrote:
               | People do complain, like they do from things like
               | systemd. Then there is us, the silent majority, who just
               | get shit done with these tools.
               | 
               | I respect what zig has done here, and I will want to try
               | it out when it stabilizes. But Rust async is just fine.
        
               | com2kid wrote:
               | The alternative is to jump through a bunch of hoops to
               | hide the "coloring" behind some opaque abstraction that
               | is complicated and that will still get in the way when
               | things go wrong.
               | 
               | Everyone complained when async IO was done with
               | callbacks, so sugar was added to the callbacks, and now
               | everyone has spent over a decade complaining about what
               | flavor of sugar tastes best.
               | 
               | Y'all at Zig have a solution, I trust Zig's solution will
               | be a good one (zig is lots of fun to use as a language)
               | but at the end of the day, IO is slow, that needs to get
               | hidden somehow, or not.
               | 
               | Everyone should have to do embedded for awhile and setup
               | their own DMA controller operations. Having async IO
               | offloaded to an actual hardware block is... A different
               | type of amusing.
        
           | raincole wrote:
           | > It was an interesting read, but I guess I came away
           | confused about why "coloring" functions is a problem. Isn't
           | "coloring" just another form of static typing?
           | 
           | It is. Function coloring is static typing.
           | 
           | But people never ever agree on what to put in typing system.
           | For example, Java's checked exceptions are a form of
           | typing... and everyone hates them.
           | 
           | Anyway it's always like that. Some people find async painful
           | and say fuck it I'm going to manage threads manually. In the
           | meanwhile another bunch of people work hard to introduce
           | async to their language. Grass is always greener on the other
           | side.
        
             | vips7L wrote:
             | > But people never ever agree on what to put in typing
             | system. For example, Java's checked exceptions are a form
             | of typing... and everyone hates them.
             | 
             | I love checked exceptions. Checked errors are fantastic and
             | I think most developers would agree they want errors to be
             | in the type system, but Java as a language just hasn't
             | provided the language syntax to make them usable. They
             | haven't made it easy to "uncheck" when you can't possibly
             | handle an error. You have to write boilerplate:
             | Something s;         try {             s = something();
             | } catch (SomethingException e) {             throw new
             | RuntimeException(e);         }
             | 
             | It sucks when you face that situation a lot. In Swift this
             | is really simple:                   var s = try!
             | something();
             | 
             | Java also hasn't made them usable with lambdas even though
             | both Scala [0] and Swift have shown it's possible with a
             | sufficiently strong type system:                   try {
             | someCall(s -> {                 try {
             | another(s);                 } catch (CheckedException ex) {
             | throw new UncheckedException(ex);                 }
             | });         } catch (UncheckedException ex) {
             | // handle somehow         }
             | 
             | It sucks. I'm hopeful one day we'll get something like try!
             | or try? and better lambdas. Maybe once all resources stop
             | being poured into Valhalla.
             | 
             | [0] https://docs.scala-
             | lang.org/scala3/reference/experimental/ca...
        
           | dwattttt wrote:
           | > Isn't "coloring" just another form of static typing?
           | 
           | In a very direct way. Another example in languages that don't
           | like you ignoring errors, changing a function from infallible
           | to fallible is a breaking change, a la "it's another colour".
           | 
           | I'm glad it is: if a function I call can suddenly fail, at
           | the very least I want to know that it can, even if the only
           | thing I do is ignore it (visibly).
        
           | dminik wrote:
           | Yes, the main character of that article really is mostly
           | JavaScript. The main issue there is that some things must be
           | async, and that doesn't mesh well with things that can't be.
           | 
           | If you're writing a game, and you need to render a new enemy,
           | you might want to reduce performance by blocking rather than
           | being shot by an invisible enemy because you can only load
           | the model async.
           | 
           | But even the article acknowledges that various languages
           | tackle this problem better. Zig does a good job, but claiming
           | it's been defeated completely doesn't really fly for me.
        
           | ezst wrote:
           | I believe the point is less about "coloring" not having value
           | as a type-system feature, and more about its bad ergonomics,
           | and its viral nature in particular.
        
           | vips7L wrote:
           | > He mentions 2015 era Java as being ok, but as someone
           | that's written a lot of multithreaded Java code, it's easy to
           | mess up and people spam the "synchronized" keyword/"color"
           | everywhere as a result. I don't feel the lack of colors in
           | Java makes it particularly intuitive or conceptually simpler.
           | 
           | Async as a keyword doesn't solve this or make writing
           | parallel code any easier. You can still mess this up even if
           | every function is annotated as async.
           | 
           | > A function with only synchronous code behaves very
           | differently than one that runs code inside another thread or
           | in a different tick of the event loop.
           | 
           | I think this is conflating properties of multiple runtimes.
           | This is true in JavaScript because the runtime works on an
           | event loop. In Java an "async" function that reads from a
           | file or makes an http call doesn't run in a different threads
           | and doesn't run in a different tick of an event loop. So what
           | value does it have in that type of runtime?
           | 
           | Personally for me I think "async" is putting pain on a lot of
           | developers where 99% of all code is not parallel and doesn't
           | share memory.
        
         | ozgrakkurt wrote:
         | You are skipping the massive point here.
         | 
         | If you are using a library in rust, it has to be async await,
         | tokio, send+sync and all the other crap. Or if it is sync api
         | then it is useless for async application.
         | 
         | This approach of passing IO removes this problem and this is
         | THE main problem.
         | 
         | This way you don't have to use procedural macros or other bs to
         | implement multi versioning for the functions in your library,
         | which doesn't work well anyway in the end.
         | 
         | https://nullderef.com/blog/rust-async-sync/
         | 
         | You can find 50 other ones like this by searching.
         | 
         | To be honest I don't hope they will solve cooperative
         | scheduling, high performance, optionally thread-per-core async
         | soon and the API won't be that good anyway. But hope it solves
         | all that in the future.
        
           | dwattttt wrote:
           | > Or if it is sync api then it is useless for async
           | application.
           | 
           | The rest is true, but this part isn't really an issue. If
           | you're in an async function you can call sync functions
           | still. And if you're worried it'll block and you can afford
           | that, I know tokio offers spawn_blocking for this purpose.
        
           | tcfhgj wrote:
           | > If you are using a library in rust, it has to be async
           | await, tokio, send+sync and all the other crap
           | 
           | Send and sync is only required if you want to access
           | something from multiple threads, which isn't required by
           | async await (parallelism vs concurrency)
           | 
           | 1) You can use async await without parallelism and 2) send
           | and sync aren't a product of async/await in Rust, but
           | generally memory safety, i.e. you need Send generally when
           | something can/is allowed to move between threads.
        
             | m11a wrote:
             | Yes, but async Rust is basically built on tokio's runtime,
             | which is what most the big async libraries depend on, like
             | hyper/axum/tokio etc. And tokio is a thread-per-core work-
             | stealing architecture, which requires Send + Sync bounds
             | everywhere. You can avoid them if you depend on tokio-
             | proper, but it's more icky when building on something like
             | axum, where your application handlers also require these
             | bounds.
             | 
             | A good article on this: https://emschwartz.me/async-rust-
             | can-be-a-pleasure-to-work-w...
        
               | tcfhgj wrote:
               | Iirc I had a situation a while back, in which I used
               | async await with tokio with a non Send or Sync type and
               | it compiled when I didn't use spawn[1] (implying
               | multithreading) but a simple loop with sequential
               | processing.
               | 
               | Only when I wanted to enable parallelism using spawn, I
               | got a compilation error.
               | 
               | [1]:
               | https://docs.rs/tokio/latest/tokio/task/fn.spawn.html
        
             | WhyNotHugo wrote:
             | > Send and sync is only required if you want to access
             | something from multiple threads, which isn't required by
             | async await (parallelism vs concurrency)
             | 
             | In theory this is correct. In practice, a lot of APIs
             | (including many in tokio) require both traits even for
             | single-thread use cases.
        
           | dminik wrote:
           | I'm not skipping anything. And in fact acknowledge this exact
           | point:
           | 
           | > That being said, I don't think Zig's implementation here is
           | bad. If anything, it does a great job at abstracting the
           | usage from the implementation. This is something Rust fails
           | at spectacularly.
        
           | junon wrote:
           | This comment is justly flatly incorrect. You don't need Tokio
           | at all to write an async library. Nor do you need send sync.
           | Not sure what other crap you are speaking of, either.
           | 
           | Sync APIs can be spawned in worker threads as futures, too.
           | Generally executors have helper methods for that.
        
         | flohofwoe wrote:
         | > In order to call such a function you also need to provide the
         | context. Zig hasn't really solved this.
         | 
         | It _is_ much more flexible though since you don 't need to pass
         | the IO implementation into each function that needs to do IO.
         | You could pass it once into an init function and then use that
         | IO impl throughout the object or module. Whether that's good
         | style is debatable - the Zig stdlib currently has containers
         | that take an allocator in the init function, but those are on
         | the way out in favour of explicitly taking the allocator in
         | each function that needs to allocate - but the user is still
         | free to write a minimal wrapper to restore the 'pass allocator
         | into init' behaviour.
         | 
         | Odin has an interesting solution in that it passes an implicit
         | context pointer into each function, but I don't know if the
         | compiler is clever enough to remove the overhead for called
         | functions that don't access the context (since it also needs to
         | peek into all called functions - theoretically Zig with it's
         | single-compilation-unit approach could probably solve that
         | problem better).
        
           | tcfhgj wrote:
           | You can write a wrapper in other langs, too, e.g. in Rust:
           | block_on(async_fn)
        
         | nurettin wrote:
         | I think the point is 3 doesn't fully apply anymore. And that
         | was the main pain point. You couldn't call a blue in red even
         | if it didn't use IO without some kind of execution wrapper or
         | waiter. Now you clearly can.
        
         | tcfhgj wrote:
         | > If anything, it does a great job at abstracting the usage
         | from the implementation. This is something Rust fails at
         | spectacularly.
         | 
         | Could you expand on this? I don't get what you mean
        
           | koito17 wrote:
           | I am not very experienced in async Rust, but it seems there
           | are some pieces of async Rust that rely too much on tokio
           | internals, so using an alternative runtime (like pollster)
           | results in broken code.
           | 
           | Searching for comments mentioning "pollster" and "tokio" on
           | HN brings a few results, but not one I recall seeing a while
           | ago where someone demonstrated an example of a library (using
           | async Rust) that crashes when not using tokio as the
           | executor.
           | 
           | Related documentation: https://rust-lang.github.io/async-
           | book/08_ecosystem/00_chapt...
        
             | tcfhgj wrote:
             | thanks, got it!
        
             | pimeys wrote:
             | Yep. The old async wars in the Rust ecosystem. It's the
             | AsyncRead and AsyncWrite traits. Tokio has its own, there
             | was a standard brewing at the same time in the futures
             | crate. Tokio did their own thing, people burnt out, these
             | traits were never standardized to std.
             | 
             | So you cannot use most of the async crates easily outside
             | Tokio.
        
             | conradludgate wrote:
             | There's two details that are important to highlight. tokio
             | is actually 2 components, it's the async scheduler, and
             | it's the IO runtime. Pollster is only a scheduler, and does
             | not offer any IO functionality. You can actually use tokio
             | libraries with pollster, but you need to register the IO
             | runtime (and spawn a thread to manage it) - this is done
             | with Runtime::enter() and it configures the thread local
             | interface so any uses of tokio IO know what runtime to use.
             | 
             | There are ideas to abstract the IO runtime interface into
             | the async machinery (in Rust that's the Context object that
             | schedulers pass into the Future) but so far that hasn't
             | gotten anywhere.
        
           | dminik wrote:
           | Sure. Let's do an imaginary scenario. Let's say that you are
           | the author of a http request library.
           | 
           | Async hasn't been added yet, so you're using
           | `std::net::TcpStream`.
           | 
           | All is well until async comes along. Now, you have a problem.
           | If you use async, your previous sync users won't be able to
           | (easily) call your functions. You're looking at an API
           | redesign.
           | 
           | So, you swallow your pride and add an async variant of your
           | functionality. Since Tokio is most popular, you use
           | `tokio::net::TcpStream`.
           | 
           | All is well, until a user comes in and says "Hey, I would
           | like to use your library with smol (a different async
           | runtime)". Now what do you do? Add a third variant of your
           | code using `smol::net::TcpStream`? It's getting a bit
           | ridiculous, and smol isn't the only alternative runtime.
           | 
           | One solution is to do what Zig does, but there isn't really
           | an agreed upon solution. The stdlib does not even provide
           | AsyncRead/AsyncWrite so you could invert your code and just
           | work with streams provided from above and keep your libary
           | executor agnostic.
        
         | benreesman wrote:
         | I'll let a real category theorist get into the details that
         | I'll likely flub, but the IO monad is where you end up if you
         | start on this path. That context can be implicit, but it's
         | there, and if you want any help from the compiler (to, for
         | example, guide Claude Code towards useful outcomes) you've got
         | to reify it as a real thing in the formality of the system.
         | 
         | Async and coroutines are the graveyard of dreams for systems
         | programming languages, and Andrew by independently
         | rediscovering the IO monad and getting it right? Hope of a
         | generation.
         | 
         | Functions in the real world have colors: you can have
         | predictable rules for moving between colors, or you can wing it
         | and get C++ co_await and tokio and please kill me.
         | 
         | This is The Way.
        
           | throwawaymaths wrote:
           | it's not a monad, since you can do unholy (for fp) things
           | with it, like stash it in a struct and pass the struct around
           | (even to functions which have no clue theres an io call) or
           | just grab a globalized io object and use that at will
           | arbitrarily at many entrypoints in your function.
           | 
           | most importantly, besides the obvious situations (creating
           | the io object, binding it to another object), it's not
           | generally going to be returned from a function as part of its
           | value.
        
             | benreesman wrote:
             | i'm relatively confident that andrew will happily break the
             | language again (god love him for that) when it becomes
             | clear that you really want that algebra.
             | 
             | though i will say, for a systems language, it's probably
             | better to invert the lift/unlift relationship, default to
             | do-notation and explicitly unlift into pure functions.
             | that's almost what const meant in C++ to begin with but it
             | lost it's way.
        
               | throwawaymaths wrote:
               | you're just spreading FUD about zig breaking before 1.0.
               | there is zero basis for you to be "relatively confident"
               | about this matter, because
               | 
               | 1. the io situation is basically restructuring analgously
               | to the allocator situation, which is at this point battle
               | tested. there are currently no "monads wrapping
               | statefulness" anywhere in zig.
               | 
               | 2. It's not in zig's nature to build something because it
               | it satisifies an fp idiom. the abstractions and resulting
               | "thing that the hardware does" (at least in release
               | builds) are generally more or less obvious. the levels of
               | compiler reinterpretation to achieve functional purity
               | are not really the sort of thing that zig does. for
               | example, zig does not have a privileged "iterator" method
               | that the compiler reinterprets in a way that unrolls
               | blocks or lambdas into loops without crossing a frame
               | boundary.
        
               | Measter wrote:
               | The same Andrew who rejected even basic interfaces in
               | favour of duck-typed generics or manually written
               | vtables?
        
             | cynicalkane wrote:
             | When I write in Haskell, I find myself mentally glossing
             | the returned monadic state, along the lines of, "Oh, an M x
             | is just an x that does monady stuff to get the x". This
             | becomes natural once you get the hang of do-notation and
             | sometimes monad combinators. So I'm not really thinking
             | about the monadic state in the return value a lot.
             | 
             | It's not really any less natural than thinking stateful
             | programming, except now the state is a reified thing, which
             | I think is strictly advantageous once you get used to it.
        
         | osigurdson wrote:
         | Agree that with something like go, there is truly no function
         | coloring at all. However, since most real world async things
         | require cancellation, a context parameter is always present so
         | there is some "coloring" do to that. Still, it is much less
         | viral than C# style async await as if you don't have a context
         | in your call stack you can still create one when needed and
         | call the function. I don't think it is reasonable to abstract
         | cancellation in a way that nothing has to be passed in so
         | perhaps the approach presented here is realistically as good as
         | it gets.
        
       | the__alchemist wrote:
       | Et tu, Zig?
        
       | do_not_redeem wrote:
       | I'm generally a fan of Zig, but it's a little sad seeing them go
       | all in on green threads (aka fibers, aka stackful coroutines).
       | Rust got rid of their Runtime trait (the rough equivalent of
       | Zig's Io) before 1.0 because it performed badly. Languages and
       | OS's have had to learn this lesson the hard way over and over
       | again:
       | 
       | https://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p13...
       | 
       | > While fibers may have looked like an attractive approach to
       | write scalable concurrent code in the 90s, the experience of
       | using fibers, the advances in operating systems, hardware and
       | compiler technology (stackless coroutines), made them no longer a
       | recommended facility.
       | 
       | If they go through with this, Zig will probably top out at "only
       | as fast as Go", instead of being a true performance competitor. I
       | at least hope the old std.fs sticks around for cases where
       | performance matters.
        
         | dundarious wrote:
         | It's hardly "all-in" if it is merely one choice of many, and
         | the choice is made in the executable not in the library code.
        
           | do_not_redeem wrote:
           | I have definitely gotten the impression that green threads
           | will be the favored implementation, from listening to core
           | team members and hanging around the discord. Stackless
           | coroutines don't even exist in the language currently.
        
             | andyferris wrote:
             | In the 2026 roadmap talk Andrew Kelley spoke of the fact
             | that stackless coroutines with iouring is the end goal here
             | (but the requires an orthogonal improvement in the compiler
             | for inlining that data to the stack where possible).
        
               | do_not_redeem wrote:
               | Do you have the timestamp? I watched that video when it
               | came out and don't remember hearing it.
        
             | dundarious wrote:
             | What does "favored" mean if event loop and direct blocking
             | are relatively trivial and provided also/ If I can
             | trivially use them, what do I care what Andrew or someone
             | in core thinks? The control is all mine, and near zero cost
             | (potential vtable indirection).
             | 
             | And would Rust be "all-in" if tokio was in std, so you
             | could use its tasks everywhere? That would be a very
             | similar level of "all-in" to Zig's current plan, but with a
             | seemingly better API.
             | 
             | I understand the benefit of not being in std, but really
             | not a fundamental issue, IMO.
        
             | geodel wrote:
             | > Stackless coroutines don't even exist in the language
             | currently.
             | 
             | And green thread exists in language?
        
         | andyferris wrote:
         | It actually has much the same benefits of Rust removing green
         | threads and replacing them with a generic async runtime.
         | 
         | The point here is that "async stuff is IO stuff is async
         | stuff". So rather than thinking of having pluggable async
         | runtimes (tokio, etc) Zig is going with pluggable IO runtimes
         | (which is kinda the equivalent of "which subset of libc do you
         | want to use?").
         | 
         | But in both moves the idea is to remove the runtime out of the
         | language and into userspace, while still providing a common
         | pluggable interface so everyone shares some common ground.
        
         | mlugg wrote:
         | I'm not sure how you got the perception that we're going "all
         | in" on green threads, given that the article in OP explicitly
         | mentions that we're hoping to have an implementation based on
         | stackless coroutines, based on this Zig language proposal:
         | https://github.com/ziglang/zig/issues/23446
         | 
         | Performance matters; we're not planning to forget that. If
         | fibers turn out to have unacceptable performance
         | characteristics, then they won't become a widely used
         | implementation. Nothing discussed in this article precludes
         | stackless coroutines from backing the "general purpose" `Io`
         | implementation if that turns out to be the best approach.
        
           | do_not_redeem wrote:
           | Does the BDFL want this though, or is it just one person's
           | opinion that it might be nice? Given how he has been
           | aggressively pruning proposals, I don't put any hope in them
           | anymore unless I see some kind of positive signal from him
           | directly.
           | 
           | e.g. I'd feel a lot more confident if he had made the
           | coroutine language feature a hard dependency of the
           | writergate refactor.
        
             | kristoff_it wrote:
             | mlugg is in the Zig core team
        
               | do_not_redeem wrote:
               | I'm aware, but Zig isn't a democracy where the core team
               | votes, right? Has Andrew actually expressed that he wants
               | the proposal? Without that we're left with scraps like
               | this commit message where he seems ambivalent. https://gi
               | thub.com/ziglang/zig/commit/d6c90ceb04f8eda7c6b711...
               | 
               | Andrew, I know you read these threads sometimes, give us
               | a sign so I can go down the mountain with my stone
               | tablets and tell the people whether we'll have coroutines
        
               | kristoff_it wrote:
               | we do build internal consensus before publishing articles
               | like this one, or doing other public communication.
        
               | mlugg wrote:
               | We don't _know_ whether or not we 'll have stackless
               | coroutines; it's _possible_ that we hit design problems
               | we didn 't foresee. However, at this moment, the general
               | consensus is that we are interested in pursuing stackless
               | coroutines.
               | 
               | While Andrew has the final say, as Loris points out, we
               | always work to reach a consensus internally. The article
               | lists this an an implementation that will probably exist,
               | because we agree that it probably will; nobody is
               | _promising_ it, because we also agree that it isn 't
               | guaranteed.
               | 
               | Also, bear in mind that even if stackless coroutines
               | don't make it into Zig, you can always use a single-
               | threaded blocking implementation of `Io`, so you need not
               | be negatively affected by any potential downsides to
               | fibers either way.
               | 
               | This new `Io` approach has made it strictly _more_ likely
               | than it previously was that stackless coroutines become a
               | part of Zig 's final design.
        
               | comex wrote:
               | But how will that actually work? Your stackless
               | coroutines proposal talks about explicit primitives for
               | defining a coroutine. But what about a function that's
               | not designed for any particular implementation strategy -
               | it just takes an Io and passes it on to some other
               | functions? Will the compiler have a way to compile it as
               | either sync or async, like apparently it did before? It
               | would have to, if you want to avoid function colors. But
               | your proposal doesn't explain anything about that.
               | 
               | Disclaimer: I'm not actually a Zig user, but I am very
               | interested in the design space.
        
               | mlugg wrote:
               | Right, the proposal doesn't discuss the implementation
               | details -- I do apologise if that made it seem a little
               | hand-wavey. I opted not to discuss them there, because
               | they're similar-ish to the way we lowered stackless async
               | in its stage1 implementation, and hence not massively
               | interesting to discuss.
               | 
               | The idea is that, yes, the compiler will infer whether or
               | not a function is async (in the stackless async sense)
               | based on whether it has any "suspension point", where a
               | suspension point is either: * Usage of `@asyncSuspend` *
               | A call to another async function
               | 
               | Calls through function pointers (where we typically
               | wouldn't know what we're calling, and hence don't know
               | whether or not it's async!) are handled by a new language
               | feature which has already been accepted; see a comment I
               | left a moment ago [1] for details on that.
               | 
               | If the compiler infers a function to be async, it will
               | lower it differently; with each suspension point becoming
               | a boundary where any stack-local state is saved to the
               | async frame, as well as an integer indicating where we
               | are in the function, and we jump to different code to be
               | resumed once it finishes. The details of this depend on
               | specifics of the proposal (which I'm planning to change
               | soon) and sometimes melt my brain a little, so I'll leave
               | them unexplained for now, but can probably elaborate on
               | them in the issue thread at some point.
               | 
               | Of course, this analysis of whether a function is async
               | is a little bit awkward, because it is a whole-program
               | analysis; a change in a leaf function in a little file in
               | a random helper module could introduce asynchronocity
               | which propagates all the way up to your `pub fn main`. As
               | such, we'll probably have different strategies for this
               | inference in the compiler depending on the release mode:
               | 
               | * In Debug mode, it may be a reasonable strategy to just
               | assume that (almost) all functions are asynchronous (it's
               | safe to lower a synchronous function as asynchronous,
               | just not vice versa). The overhead introduced by the
               | async lowering will probably be fairly minimal in the
               | context of a Debug build, and this will speed up build
               | times by allowing functions to be sent straight to the
               | code generator (like they are today) without having to
               | wait for other functions to be analyzed (and without
               | potentially having to codegen again later if we "guessed
               | wrong").
               | 
               | * In Release[Fast,Small,Safe] mode, we might hold back
               | code generation until we know for sure, based on the
               | parts of the call graph we have analyzed, whether or not
               | a function is async. Vtables might be a bit of a problem
               | here, since we don't know for sure that a vtable call is
               | not async until we've finished literally _all_ semantic
               | analysis. Perhaps we 'll make a guess about whether such
               | functions are async and re-do codegen later if that guess
               | was wrong. Or, in the worst case... perhaps we'll
               | literally just defer all codegen until semantic analysis
               | completes! After all, it's a release build, so you're
               | going to be waiting a while for optimizations anyway; you
               | won't mind an extra couple of seconds on delayed codegen.
               | 
               | [1]: https://news.ycombinator.com/item?id=44549131
        
               | Icathian wrote:
               | > a change in a leaf function in a little file in a
               | random helper module could introduce asynchronocity which
               | propagates all the way up to your `pub fn main`
               | 
               | If this doesn't make the argument that Zig has certainly
               | not defeated function coloring, I don't know what would.
               | 
               | The fact that this change to how my program runs is
               | hidden away in the compiler instead of somewhere visible
               | is not an improvement.
        
               | kristoff_it wrote:
               | > If this doesn't make the argument that Zig has
               | certainly not defeated function coloring, I don't know
               | what would.
               | 
               | if you're opting into stackless coroutines then yeah
               | you're opting into their viral nature, but the point is
               | that _you don 't have to_. as the application author your
               | dependencies won't opt you forcefully in using stackless
               | coroutines (or any singular execution model), which is
               | currently the case with other languages.
               | 
               | this is what it means to defeat function coloring.
        
               | hitekker wrote:
               | Small marketing suggestion: maybe " _limit_ function
               | coloring " instead of " _defeat_ function coloring ". I
               | like Zig's approach so far, but clearer terms would help
               | avoid pointless arguments and disappointments that a
               | certain proglang supremacist community loves.
        
               | SkiFire13 wrote:
               | > it's safe to lower a synchronous function as
               | asynchronous
               | 
               | Is it though? I believe this would be an issue if you
               | want to pass that function as a function pointer to an
               | FFI function, in which case it must be sync.
        
           | ksec wrote:
           | That is lovely to hear. I think the general conscious is that
           | not a single programming language has done Async right. So
           | people are a little sceptical. But Andrew and the team so far
           | seems to have the do it right mentality. So I guess people
           | should be a little more optimistic.
           | 
           | Cant wait for 0.15 coming out soon.
        
             | krior wrote:
             | I would argue that Java's async is pretty nifty, especially
             | now that some of the rough edges have been sanded off.
        
             | sesm wrote:
             | What is wrong with async in JS?
        
               | forrestthewoods wrote:
               | You have to write JS
        
         | nsm wrote:
         | I'm confused about the assertion that green threads perform
         | badly. 3 of the top platforms for high concurrency servers use
         | or plan to use green threads (Go, Erlang, Java). My
         | understanding was that green threads have limitations with C
         | FFI which is why lower level languages don't use them (Rust).
         | Rust may also have performance concerns since it has other
         | constraints to deal with.
        
           | yxhuvud wrote:
           | Green threads have issues with C FFI mostly due to not being
           | able to preempt execution, when the C thing is doing
           | something that blocks. This is a problem when you have one
           | global pool of threads that execute everything. To get around
           | it you essentially need to set up a dedicated thread pool to
           | handle those c calls.
           | 
           | Which may be fine - go doesn't let the user directly create
           | thread pools directly but do create one under the hood for
           | ffi interaction.
        
             | layer8 wrote:
             | The problem is when C calls expect to be on a particular
             | thread. Either the main event thread, in a GUI context, or
             | the same thread as some related previous call.
        
               | ori_b wrote:
               | The problem is that C expects to have enough stack to put
               | stuff there, but green threads allocate small stacks to
               | reduce (virtual) memory use.
        
               | layer8 wrote:
               | That's not a problem when a dedicated thread pool is used
               | as mentioned by GP. However they don't solve the thread
               | affinity issue.
        
         | flohofwoe wrote:
         | > I'm generally a fan of Zig, but it's a little sad seeing them
         | go all in on green threads
         | 
         | Read the article, you can use whatever approach you want by
         | writing an implementation for the IO interface, green threading
         | is just one of them.
        
         | oasisaimlessly wrote:
         | That paper (P1364R0) was contentious, and I regard it as being
         | severely motivated reasoning, published only to kill off
         | competing approaches to C++ coroutines.
         | 
         | Some discussions of that paper:
         | 
         | *
         | https://old.reddit.com/r/cpp/comments/1jwlur9/stackful_corou...
         | 
         | *
         | https://old.reddit.com/r/programming/comments/dgfxde/fibers_...
        
         | forrestthewoods wrote:
         | Oh man. I think the biggest mistake Rust has ever made is their
         | async model. It's been nothing short of a disaster. Zig
         | supporting green threads and other options is spectacularly
         | exciting. Thus far no one has "solved" async. So very exciting
         | to see what Zig can come up with.
        
       | henrikl wrote:
       | Seeing a systems language like Zig require runtime polymorphism
       | for something as common as standard IO operations seems off to me
       | -- why force that runtime overhead on everyone when the concrete
       | IO implementation could be known statically in almost all
       | practical cases?
        
         | do_not_redeem wrote:
         | I think it's just the Zig philosophy to care more about binary
         | size than speed. Allocators have the same tradeoff,
         | ArrayListUnmanaged is not generic over the allocator, so every
         | allocation uses dynamic dispatch. In practice the overhead of
         | allocating or writing a file will dwarf the overhead of an
         | indirect call. Can't argue with those binary sizes.
         | 
         | (And before anyone mentions it, devirtualization is a myth,
         | sorry)
        
           | kristoff_it wrote:
           | > (And before anyone mentions it, devirtualization is a myth,
           | sorry)
           | 
           | In Zig it's going to be a language feature, thanks to its
           | single unit compilation model.
           | 
           | https://github.com/ziglang/zig/issues/23367
        
             | do_not_redeem wrote:
             | Wouldn't this only work if there's only one implementation
             | throughout the entire compliation unit? If you use 2
             | allocators in your app, your restricted function type has 2
             | possible callees for each entry, and you're back to the
             | same problem.
        
               | Zambyte wrote:
               | > A side effect of proposal #23367, which is needed for
               | determining upper bound stack size, is guaranteed de-
               | virtualization when there is only one Io implementation
               | being used (also in debug builds!).
               | 
               | > In the less common case when a program instantiates
               | more than one Io implementation, virtual calls done
               | through the Io interface will not be de-virtualized, as
               | that would imply doubling the amount of machine code
               | generated, creating massive code bloat.
               | 
               | From the article
        
               | yxhuvud wrote:
               | I wonder how massive it actually would be. I'm guessing
               | it really wouldn't be all that massive in practice even
               | if it of course is easy to create massive examples using
               | ways people typically don't write code.
        
               | throwawaymaths wrote:
               | > Wouldn't this only work if there's only one
               | implementation throughout the entire compliation unit
               | 
               | in practice how often are people using more than one io
               | in a program?
        
               | latch wrote:
               | I think having a thread pool on top of some evented IO
               | isn't _that_ uncommon.
               | 
               | You might have a thread pool doing some very specific
               | thing. You can do your own threadpool which wont use the
               | Io interface. But if one of the tasks in the threadpool
               | wanted to read a file, I guess you'd have to pass in the
               | blocking Io implementation.
        
               | throwawaymaths wrote:
               | one of the io interfaces provided is a standard
               | threadpool io. and if it was really important, you could
               | write your own io interface that selects between std
               | threadpool and std blocking based off of an option (i am
               | guessing, i don't know, but seems reasonable)
        
               | NobodyNada wrote:
               | In larger Rust applications or servers I find myself
               | doing this very often -- for example, one application I'm
               | working on mostly uses blocking I/O for occasional
               | filesystem access but has a little bit of async
               | networking.
        
               | thrwyexecbrain wrote:
               | Having a limited number of known callees is already
               | better than a virtual function (unrestricted function
               | pointer). A compiler in theory could devirtualize every
               | two-possible-callee callsite into `if (function_pointer
               | == callee1) callee1() else callee2()` which then can be
               | inlined at compile time or branch-predicted at runtime.
               | 
               | In any case, if you have two different implementations of
               | something then you have to switch between them somewhere
               | -- either at compile-time or link-time or load-time or
               | run-time (or jit-time). The trick is to find an
               | acceptable compromise of performance, (machine)code-bloat
               | and API-simplicity.
        
           | ozgrakkurt wrote:
           | It can also mean faster compilation (and sometimes better
           | performance? https://nical.github.io/posts/rust-custom-
           | allocators.html)
           | 
           | Just templating everything doesn't mean it will be faster
           | every time
        
           | lerno wrote:
           | > care more about binary size than speed
           | 
           | That does not seem to be true if you look at how string
           | formatting is implemented.
        
         | nu11ptr wrote:
         | I/O strikes me as one place where dynamic dispatch overhead
         | would likely be negligible in practice. Obviously it depends on
         | the I/O target and would need to be measured, but they don't
         | call them "I/O bound" (as opposed to "CPU bound") programs for
         | no reason.
        
         | throwawaymaths wrote:
         | > why force that runtime overhead on everyone
         | 
         | pretty sure the intent is for systems that only use one io to
         | have a compiler optimization that elides the cost of double
         | indirection... but also, you're doing IO! so usually something
         | else is the bottleneck, one extra indirection is likely to be
         | peanuts.
        
         | ozgrakkurt wrote:
         | Runtime polymorphism isn't something inherently bad.
         | 
         | It is bad if you are introducing branching in a tight loop or
         | preventing compiler from inlining things it would inline
         | otherwise and other similar things maybe?
        
       | didibus wrote:
       | I don't know Zig, but wouldn't such a change be a major breaking
       | change where all prior Zig code doing Io wouldn't work anymore if
       | upgraded?
        
         | open592 wrote:
         | Large breaking change:
         | 
         | https://github.com/ziglang/zig/pull/24329
        
         | xxpor wrote:
         | Zig's not at 1.0 yet, so there's no stability guarantee at this
         | point.
        
         | TUSF wrote:
         | Breaking changes is just another Tuesday for Zig.
        
         | flohofwoe wrote:
         | Yeah, but why is that a problem? Zig doesn't promise any
         | stability before 1.0, and it's not like we don't need to change
         | code in other language ecosystem frequently for all sorts of
         | reasons (e.g. bumping a dependency version, or a new minor
         | C/C++ compiler implementing new warnings).
        
       | sevensor wrote:
       | > io.async expresses asynchronicity (the possibility for
       | operations to happen out of order and still be correct) and it
       | does not request concurrency, which in this case is necessary for
       | the code to work correctly.
       | 
       | This is the key point for me. Regardless of whether you're under
       | an async event loop, you can specify that the order of your io
       | calls does not imply sequencing. Brilliant. Separate what async
       | means from what the io calls do.
        
       | gavinhoward wrote:
       | As the author of a semi-famous post about how Zig has function
       | colors [1], I decided to read up on this.
       | 
       | I see that blocking I/O is an option:
       | 
       | > The most basic implementation of `Io` is one that maps to
       | blocking I/O operations.
       | 
       | So far, so good, but blocking I/O is not async.
       | 
       | There is a thread pool that uses blocking I/O. Still good so far,
       | but blocking I/O is still not async.
       | 
       | Then there's green threads:
       | 
       | > This implementation uses `io_uring` on Linux and similar APIs
       | on other OSs for performing I/O combined with a thread pool. The
       | key difference is that in this implementation OS threads will
       | juggle multiple async tasks in the form of green threads.
       | 
       | Okay, they went the Go route on this one. Still (sort of) not
       | async, but there is an important limitation:
       | 
       | > This implementation requires having the ability to perform
       | stack swapping on the target platform, meaning that it will not
       | support WASM, for example.
       | 
       | But still no function colors, right?
       | 
       | Unfortunately not:
       | 
       | > This implementation [stackless coroutines] won't be available
       | immediately like the previous ones because it depends on
       | reintroducing a _special function calling convention_ and
       | rewriting function bodies into state machines that don't require
       | an explicit stack to run.
       | 
       | (Emphasis added.)
       | 
       | And the function colors appear again.
       | 
       | Now, to be fair, since there are multiple implementation options,
       | you _can_ avoid function colors, especially since `Io` is a
       | value. But those options are either:
       | 
       | * Use blocking I/O.
       | 
       | * Use threads with blocking I/O.
       | 
       | * Use green threads, which Rust removed [2] for good reasons [3].
       | It only works in Go because of the garbage collector.
       | 
       | In short, the real options are:
       | 
       | * Block (not async).
       | 
       | * Use green threads (with their problems).
       | 
       | * Function colors.
       | 
       | It doesn't appear that the function colors problem has been
       | defeated. Also, it appears to me that the Zig team decided to
       | have every concurrency technique in the hope that it would appear
       | innovative.
       | 
       | [1]: https://gavinhoward.com/2022/04/i-believe-zig-has-
       | function-c...
       | 
       | [2]: https://github.com/aturon/rfcs/blob/remove-
       | runtime/active/00...
       | 
       | [3]: https://www.open-
       | std.org/JTC1/SC22/WG21/docs/papers/2018/p13...
        
         | ozgrakkurt wrote:
         | Their bet seems to be that they can transparently implement
         | real async inside an IO implementation using compiler magic.
         | But then it means if you use that IO instance with the magic
         | then your function gets transformed into a state machine?
         | 
         | Then this whole thing is useless for implementing cooperative
         | scheduling async like in rust?
        
           | flohofwoe wrote:
           | > But then it means if you use that IO instance with the
           | magic then your function gets transformed into a state
           | machine?
           | 
           | This was essentially like the old async/await implementation
           | in Zig already worked. The same function gets the state-
           | machine treatment if it was called in an async context,
           | otherwise it's compiled as a 'regular' sequential function.
           | 
           | E.g. at runtime there may be two versions of a function, but
           | not in the code base. Not sure though how that same idea
           | would be implemented with the new IO interface, but since Zig
           | strictly uses a single-compilation-unit model the compiler
           | might be able to trace the usage of a specific IO
           | implementation throughout the control flow?
        
           | yxhuvud wrote:
           | No, it just means the cooperative scheduler needs to provide
           | an io implementation that works with the rest of the
           | scheduler.
        
         | mlugg wrote:
         | > it depends on _reintroducing a special function calling
         | convention_
         | 
         | This is an internal implementation detail rather than a fact
         | which is usually exposed to the user. This is essentially just
         | explaining that the Zig compiler needs to figure out which
         | functions are async and lower them differently.
         | 
         | We do have an explicit calling convention,
         | `CallingConvention.async`. This was necessary in the old
         | implementation of async functions in order to make runtime
         | function pointer calls work; the idea was that you would cast
         | your `fn () void` to a `fn () callconv(.async) void`, and then
         | you could call the resulting `*const fn () callconv(.async)
         | void` at runtime with the `@asyncCall` builtin function. This
         | was one of the biggest flaws in the design; you could argue
         | that it introduced a form of coloring, but in practice it just
         | made vtables incredibly undesirable to use, because (since
         | nobody was actually doing the `@asyncCall` machinery in their
         | vtable implementations) they effectively just didn't support
         | async.
         | 
         | We're solving this with a new language feature [0]. The idea
         | here is that when you have a virtual function -- for a simple
         | example, let's say `alloc: *const fn (usize) ?[*]u8` -- you
         | instead give it a "restricted function pointer type", e.g.
         | `const AllocFn = @Restricted(*const fn (usize) ?[*]u8);` with
         | `alloc: AllocFn`. The magic bit is that the compiler will track
         | the full set of comptime-known function pointers which are
         | coerced to `AllocFn`, so that it can know the full set of
         | possible `alloc` functions; so, when a call to one is
         | encountered, it knows whether or not the callee is an async
         | function (in the "stackless async" sense). Even if some `alloc`
         | implementations are async and some are not, the compiler can
         | literally lower `vtable.alloc(123)` to `switch (vtable.alloc) {
         | impl1 => impl1(123), impl2 => impl2(123), ... }`; that is, it
         | can look at the pointer, and determine from that whether it
         | needs to dispatch a synchronous or async call.
         | 
         | The end goal is that _most_ function pointers in Zig should be
         | used as restricted function pointers. We 'll probably keep
         | normal function pointers around, but they ideally won't be used
         | at all often. If normal function pointers are kept, we might
         | keep `CallingConvention.async` around, giving a way to call
         | them as async functions if you _really_ want to; but to be
         | honest, my personal opinion is that we probably _shouldn 't_ do
         | that. We end up with the constraint that unrestricted pointers
         | to functions where the compiler has inferred the function as
         | async (in a stackless sense) cannot become runtime-known, as
         | that would lead to the compiler losing track of the calling
         | convention it is using internally. This would be a very rare
         | case provided we adequately encourage restricted function
         | pointers. Hell, perhaps we'd just ban _all_ unrestricted
         | default-callconv function pointers from becoming runtime-known.
         | 
         | Note also that stackless coroutines _do_ some with a couple of
         | inherent limitations: in particular, they don 't play nicely
         | with FFI (you can't suspend across an FFI boundary; in other
         | words, a function with a well-defined calling convention like
         | the C calling convention is not allowed to be inferred as
         | async). This is a limitation which seems perfectly acceptable,
         | and yet I'm very confident that it will impact significantly
         | _more_ code than the calling convention thing might.
         | 
         | TL;DR: depending on where the design ends up, the "calling
         | convention" mentioned is either entirely, or almost entirely,
         | just an implementation detail. Even in the "almost entirely"
         | case, it will be exceptionally rare for anyone to write code
         | which could be affected by it, to the point that I don't think
         | it's a case worth seriously worrying about unless it proves
         | itself to actually be an issue in practice.
         | 
         | [0]: https://github.com/ziglang/zig/issues/23367
        
           | gavinhoward wrote:
           | From my experience, the calling convention was, in 0.9.x,
           | just an implementation detail, until it wasn't. I think I may
           | still reserve judgment for when async is fully implemented.
           | Then I'll torture it again.
        
         | audunw wrote:
         | It's hard to judge before stackless coroutines are
         | reintroduced. But I think you're entirely wrong that the next
         | version of it will have colored functions, even according to
         | your definition.
         | 
         | It has been mentioned that it's possible that the default for
         | debug builds is that every single function is compiled as an
         | async function. I.e. there is canonically only one function
         | color. Changing function color could become an optimisation for
         | release builds. This is really not much different from inlining
         | functions or other tricks the compiler can do with the calling
         | convention if it has perfect knowledge of all callers.
         | 
         | > it appears to me that the Zig team decided to have every
         | concurrency technique in the hope that it would appear
         | innovative.
         | 
         | That's a _really_ bad take. It's not much different from what
         | they did to make allocators explicit. It's an excellent
         | approach for what Zig is supposed to be. Different concurrency
         | models have different performance tradeoffs, just like with
         | allocators. If the can support different IO models without
         | making the language complicated, that's a huge win, and they
         | seem to be achieving that.
         | 
         | I find this approach the opposite of "appear innovative".
         | They've moved away from designing in a bunch of fancy syntax
         | that locks users into one particular concurrency model, and
         | gone for a more explicit and boring design which puts power in
         | the hands of the user. It may not be right for everyone, but
         | for what Zig is setting out to do it's perfect. A disciplined
         | decision in my opinion.
         | 
         | Getting stackless coroutines right for a low level language
         | like Zig would be somewhat innovative. But not in a way that's
         | flashy or super interesting.
        
           | gavinhoward wrote:
           | > It has been mentioned that it's possible that the default
           | for debug builds is that every single function is compiled as
           | an async function. I.e. there is canonically only one
           | function color.
           | 
           | But then, for those that choose to only use blocking I/O or
           | green threads, they still pay the penalty of async merely
           | existing.
           | 
           | > That's a _really_ bad take. It's not much different from
           | what they did to make allocators explicit.
           | 
           | I mean, Zig explicit allocators are really the same thing is
           | Go interfaces, just dressed up as an innovative feature by a
           | specific use case. This is what I mean by "appearing"
           | innovative: they are taking old and tested ideas and
           | presenting them in new ways to make them appear new.
           | 
           | Also, Zig could have had explicit allocators without needing
           | to pass them to every function [1].
           | 
           | > They've moved away from designing in a bunch of fancy
           | syntax that locks users into one particular concurrency
           | model, and gone for a more explicit and boring design which
           | puts power in the hands of the user.
           | 
           | Except that if every function is made async, they have
           | actually removed from users the power to choose to _not_ use
           | async.
           | 
           | [1]: https://jai.community/t/context/163
        
       | eestrada wrote:
       | Although I'm not wild about the new `io` parameter popping up
       | everywhere, I love the fact that it allows multiple
       | implementations (thread based, fiber based, etc.) and avoids
       | forcing the user to know and/or care about the implementation,
       | much like the Allocator interface.
       | 
       | Overall, I think it's a win. Especially if there is a stdlib
       | implementation that is a no overhead, bogstock, synchronous,
       | blocking io implementation. It follows the "don't pay for things
       | you don't use" attitude of the rest of zig.
        
         | ozgrakkurt wrote:
         | Isn't "don't pay for what you don't use" a myth? Some other
         | person will using unless you are a very small team with
         | discipline, and you will pay for it.
         | 
         | Or just passing around an "io" is more work than just calling
         | io functions where you want them.
        
       | aatd86 wrote:
       | So is that zig becoming a type AND effect system?
        
       | phplovesong wrote:
       | I wish Zig had not done async/await. CPS (like you have in Go) is
       | way, way better, and is lower level, making it possible to do you
       | own "async/await" if you really want to.
        
         | flohofwoe wrote:
         | Read the article, the new Zig async/await interface doesn't
         | imply the typical async/await state-machine code
         | transformation. You can write a simple blocking runtime, or a
         | green-thread implementation, or a thread-pool, or the state-
         | machine approach via stackless coroutines (but AFAIK this needs
         | a couple of language builtins which then must be implemented in
         | an IO implementation).
        
         | osa1 wrote:
         | By CPS do you mean lightweight threads + meeting point
         | channels? (i.e. both the reader and writer get blocked until
         | they meet at the read/write call) Or something else?
         | 
         | Why is CPS better and lower level than async/await?
        
           | burnt-resistor wrote:
           | Because it allows multiple topologies of producers and
           | consumers.
        
             | osa1 wrote:
             | No idea what that means.. Do you have a concrete example of
             | what CPS allows and async/await doesn't?
        
               | mikojan wrote:
               | I believe async/await means you have a single consumer
               | (caller) and a single producer (callee) and only a single
               | value will be produced (resolved).
               | 
               | With CPS you may send and receive many times over to
               | whomever and from whomever you like.
               | 
               | In JavaScript you may write..                   const
               | fetchData = async () => {             // snip
               | return data;         }              const data = await
               | fetchData();
               | 
               | And in Go you might express the same like..
               | channel := make(chan int);                  go func() {
               | // snip             channel <- data;         }()
               | data := <-channel
               | 
               | But you could also, for example, keep sending data and
               | send it to as many consumers as you like..
               | go func() {             for { // Infinite loop:
               | // snip                 channel1 <- data;
               | channel2 <- data;                 channel3 <- data;
               | }         }()
        
           | phplovesong wrote:
           | Async/await tend to be IO bound, in zigs case hiw can i know
           | what to use and when? Say i do a db call, thats clearly IO,
           | and later do heavy CPU thing, now how do i know that the CPU
           | thing does not block inside my async io thing?
           | 
           | I guess its pure luck if the io implementation can handle
           | both, but who knows?
        
             | jufter wrote:
             | > how do i know that the CPU thing does not block inside my
             | async io thing?
             | 
             | I think it's common sense to not interweave IO with long-
             | running CPU, hence sans IO.
             | 
             | If you want to go that route, we already have solutions:
             | goroutines and beam processes.
        
               | phplovesong wrote:
               | This was my point. With Go it does not matter, i can do
               | IO or CPU both with Gos concurrency (CSP), but with
               | async/await i usully cannot, i assume this is not
               | something Zig is planning on, as it seems to be all about
               | IO.
        
       | noelwelsh wrote:
       | Ok, they are implementing an effect system. Is there any
       | acknowledgement that they are going down an established path?
        
         | ryeats wrote:
         | When I watched the release notes they didn't sound like it was
         | some ground breaking new pattern it's just a new approach that
         | fits best with zig.
        
       | wucke13 wrote:
       | Is this in effect introducing algebraic effects by concept? E.g.
       | the io passed in is an effect handler, and it is the effect
       | handler's choice whether to perform stack switching (or other
       | means of non-blocking waiting) to enable asynchronicity?
        
         | runeks wrote:
         | In my view, algebraic effects enable specifying different
         | _kinds_ of effects (with different interpretations) -- e.g.
         | read a file, run DB query, network access -- as opposed to just
         | a single  'Io' effect that allows everything.
        
       | wordofx wrote:
       | Damn that's some ugly async syntax.
        
         | sgt wrote:
         | Not Zig philosophy to hide things away to make it prettier.
        
           | wordofx wrote:
           | You don't need to hide it.
        
             | mikojan wrote:
             | What is your alternative?
        
       | Yoric wrote:
       | Interesting. This is a bit reminiscent of how OCaml handles async
       | these days.
        
       | hardwaresofton wrote:
       | Note that this same concept is "sans io" and was previously
       | discussed for it's use in Rust:
       | 
       | https://www.firezone.dev/blog/sans-io
       | 
       | https://sans-io.readthedocs.io/
       | 
       | https://news.ycombinator.com/item?id=40872020
        
         | jwolfe wrote:
         | If the functions are still calling I/O methods directly rather
         | than the I/O being externally driven, I don't think that
         | qualifies as sans-io, based on my previous exposure / based on
         | your second link:
         | 
         | > For byte-stream based protocols, the protocol implementation
         | can use a single input buffer and a single output buffer. For
         | input (that is, receiving data from the network), the calling
         | code is responsible for delivering code to the implementation
         | via a single input (often via a method called receive_bytes, or
         | something similar). The implementation will then append these
         | bytes to its internal byte buffer. At this point, it can choose
         | to either eagerly process those bytes, or do so lazily at the
         | behest of the calling code.
         | 
         | > When it comes to generating output, a byte-stream based
         | protocol has two options. It can either write its bytes to an
         | internal buffer and provide an API for extracting bytes from
         | that buffer, as done by hyper-h2, or it can return bytes
         | directly when the calling code triggers events (more on this
         | later), as done by h11. The distinction between these two
         | choices is not enormously important, as one can easily be
         | transformed into the other, but using an internal byte buffer
         | is recommended if it is possible that the act of receiving
         | input bytes can cause output bytes to be produced: that is, if
         | the protocol implementation sometimes automatically responds to
         | the peer without user input.
        
           | matu3ba wrote:
           | Yep, that would be more like structured concurrency also
           | mentioned in linked blog post. sans-io is about state machine
           | as interface, but unfortunately does not specify a formal
           | model or how to synthesize/derive one etc.
        
       | lenkite wrote:
       | Love the no function coloring solution! I am so looking forward
       | to Zig 1.0. Finally, a system programming language that I can
       | actually read and understand without putting in heavy labor.
       | Hell, I could fully follow this blog post, without actually
       | knowing anything much about Zig. Broke my head on async Rust
       | several times before throwing in the towel.
        
       | anonymoushn wrote:
       | I think this design is a regression from the previous design, in
       | which you could use compile time introspection to check whether
       | things are actually async (calling convention) or not.
       | 
       | Additionally, I don't necessarily want to delegate the management
       | of the memory backing the futures to an Io, or pass around a blob
       | of syscalls and an associated runtime, which accesses everything
       | via a vtable. I would prefer to have these things be compile time
       | generic only.
        
         | audunw wrote:
         | Your preference to have them be compile time generic shouldn't
         | come at the cost of those that would want runtime
         | virtualisation.
         | 
         | As the article concludes, you get the best of both worlds here,
         | where the result is effectively compile time generic if you
         | only use one io implementation in your program. In theory it'd
         | also partially compile time generic if you exclusively use one
         | io for one set of libraries/functions and a different io for
         | another set of libraries/functions.
         | 
         | I see this as the objectively correct design based on the
         | existing design decisions in Zig. It follows from the allocator
         | interface decision.
        
           | anonymoushn wrote:
           | Yes, I understand that the designers prefer the Allocator
           | situation and that Reader and Writer being anytype was
           | downstream of the difficulty of using async readers and
           | writers otherwise. So the intention was always to go with the
           | design that I do not prefer. One reason I do not prefer it is
           | that the Reader and Writer interfaces were already
           | staggeringly inefficient, despite the lack or virtualization.
           | We have avoided the issue by reimplementing a bunch of their
           | API in some specific readers and writers and modifying the
           | stdlib Reader and Writer to dispatch to these methods if they
           | are present.
           | 
           | To be honest, I just do not have much faith in the commitment
           | to optimality, when it seems like the team has not spent time
           | doing things like profiling a program that spends a lot of
           | time formatting integers as decimal syrings, and noticing
           | that the vast majority of that formatting runtime is UTF-8
           | validation. I am happy to continue using the language,
           | because it makes it easy enough to fix these issues oneself.
           | 
           | The only aspect that may not be recoverable by the end user
           | is the "am I async/is this async" reflection issue, though a
           | core team member has clarified in this comment section that
           | the code in the article is a sketch and the design of
           | stackless coroutines is far from done, so we may yet get
           | this.
           | 
           | Some other philosophical point is, like, lua's
           | coroutine.create/resume/yield/clone are control flow
           | primitives for use within a single thread of execution. It's
           | fine to ship an async runtime, which embodies the view they
           | they are not control flow primitives for use within a single
           | thread of execution, for doing I/O. But focusing the
           | primitives for creating and switching between execution
           | contexts too narrowly on the async runtime use case is liable
           | to he harmful to other use cases for these operations.
           | Ideally, we would be able to write things like a prominent
           | SNES emulator that uses stack switching to ensure the
           | simulation of different components proceeds in an order known
           | to be more correct than other orders, and we would be able to
           | do it using native language features, which would compile
           | down to something a bit cheaper than dumping all of our
           | registers onto the stack. Ideally when we do this we would
           | not be asked by the language to consider what it would mean
           | to "cancel" the execution context managing one of the
           | components, in the same way that we do not need to consider
           | what it means to cancel an arbitrary struct, or the function
           | which is calling the function currently executing.
        
       | logicchains wrote:
       | Does this mean that as a side effect, it'll now be possible to
       | enforce functions are pure/deterministic in Zig by not passing in
       | an Io?
        
         | mlugg wrote:
         | Not quite:
         | 
         | * Global variables still exist and can be stored to / loaded
         | from by any code
         | 
         | * Only convention stops a function from constructing its own
         | `Io`
         | 
         | * Only convention stops a function from reaching directly into
         | low-level primitives (e.g. syscalls or libc FFI)
         | 
         | However, in practice, we've found that such conventions tend to
         | be fairly well-respected in most Zig code. I anticipate `Io`
         | being no different. So, if you see a function which doesn't
         | take `Io`, you can be pretty confident (particularly if it's in
         | a somewhat reputable codebase!) that it's not interacting with
         | the system (e.g. doing filesystem accesses, opening sockets,
         | sleeping the thread).
        
           | logicchains wrote:
           | What about random number generation; is that something that
           | will also fall under Io?
        
       | Cloudef wrote:
       | I like the IO interface simply for the fact that it would allow
       | me to create language level vfs
        
         | crabmusket wrote:
         | Seeing the example code made me wonder if this would allow
         | introducing capability based security. E.g. passing an `io`
         | instance to a library which can only read a subtree of the
         | filesystem.
         | 
         | Edit: not quite https://news.ycombinator.com/item?id=44549430
        
           | Cloudef wrote:
           | Only if you are sure all the code uses the IO instance, if
           | you mean language level sandboxing of untrusted code then no,
           | zig code can always call syscalls directly. But you can
           | compile zig to wasm which will give you capability based
           | security.
        
       | bigswede wrote:
       | Speaking of colors... I like the color scheme of the code
       | snippets, is it a standard scheme available in VSCode?
        
       | xmorse wrote:
       | This is a good time to implement "context", a way to pass down
       | the call stack parameters instead of having to add a io argument
       | to every function
        
         | sbszllr wrote:
         | I don't know if it's still true in the recent versions of Scala
         | (stopped caring in 2018) but it used to have implicit
         | parameters designed specifically for passing context like this.
         | 
         | A notable example was passing around an implicit
         | ExecutionContext for thread pools, e.g. in Akka :)
        
       | Galanwe wrote:
       | So you have to do:                   io.async(saveFile, .{io,
       | data, "saveA.txt"}).await(io);
       | 
       | That is 3 references to `io` in a single call.
       | 
       | Considering there is very little use case for mix and matching
       | different Ios, any chance of having some kind of default /
       | context IO to avoid all these ?
        
         | messe wrote:
         | If you're going to immediately await it, you can just do
         | saveFile(io, data, "saveA.txt");
         | 
         | EDIT: following up on that, I'm actually not sure that
         | io.async(saveFile, .{io, data, "saveA.txt"}).await(io);
         | 
         | will even be valid code. Futures in this article are declared
         | as var, meaning mutable. This appears to be because
         | Future.await is going to take a pointer as its initial
         | argument. However, because it's a temporary and therefore
         | treated as const, the return value of io.async will not be
         | passable to a .await function expecting a *Future as its
         | initial argument without first being stored in a mutable var.
         | 
         | So this would be valid:                   var save_future =
         | io.async(saveFile, .{io, data, "saveA.txt"});
         | save_future.await(io);
         | 
         | But the original presented in the parent comment would be
         | equivalent to the following, and therefore invalid:
         | const save_future = io.async(saveFile, .{io, data,
         | "saveA.txt"});         save_future.await(io); // compile error
        
       | rastignack wrote:
       | I wrote a simple ssh server in zig to learn the language in my
       | spare time.
       | 
       | The new design makes the event loop / io much easier to reason
       | about. Thanks Andy
        
         | runeks wrote:
         | > The new design makes the event loop / io much easier to
         | reason about.
         | 
         | Interesting. How so?
        
       | garaetjjte wrote:
       | I'm confused. The trouble with "colored" functions is that they
       | either do processing on the stack, or unwind the stack. They
       | claim defeat of function coloring, and describe that IO
       | implementation can use blocking/thread pool/green threads. But...
       | these are all blocking methods, which weren't the problem in the
       | first place! If you keep convention to never do IO using global
       | state, you could do that practically in any language. Stackless
       | coroutines being left for later feels like "draw the rest of the
       | owl" situation.
       | 
       | To actually have truly universal functions, I think there are two
       | solutions:
       | 
       | - Make every function async, and provide extra parameter
       | indicating to not actually unwind the stack and execute
       | synchronously instead. Comes with performance penalty.
       | 
       | - Compile each function twice, picking appropiate variant at call
       | site. Increases code size and requires some hackery with handling
       | function pointers.
        
         | bob1029 wrote:
         | > Make every function async, and provide extra parameter
         | indicating to not actually unwind the stack and execute
         | synchronously instead. Comes with performance penalty.
         | 
         | I think ValueTask<T> in C#/.NET can approach this use case - It
         | avoids overhead if the method actually completes synchronously.
         | Otherwise, you can get at the Task<T> if needed. From a code
         | perspective, you await it like you normally would and the
         | compiler/runtime figures out what to do.
        
         | throwawaymaths wrote:
         | I am not on the core team but i believe the plan is to do
         | exactly what you are talking about, but _after_ the API is
         | nailed down and kinks have been ironed out by users of the
         | existing semiblocking implementation (to possibly include the
         | compiler), as the default LLVM coro state machine compiler has
         | problems (for example: I think I remember Andrew complaining
         | that it has an obligatory libc /malloc dependency).
         | 
         | since the new io interface has userland async/await methods,
         | then dropping in a proper frame jumping solution will be less
         | painful, and easier to debug, and if using coroutines proves to
         | be challenging with the api hopefully changes to io api would
         | be minor, versus going after stackless coroutines NOW and
         | making large API changes often as the warts with the system
         | uncover themselves.
        
       | thrwyexecbrain wrote:
       | I miss the mention of boost::asio in this thread. At first glance
       | this new Io interface feels not that dissimilar to it:
       | 
       | Both are generic interfaces over an event loop/executor
       | supporting async or blocking operations. Both ship a thread-pool
       | and a stackful coroutine backend and both can be used through
       | their respective language's stackless coroutine implementation
       | (co_yield in cpp and yet-unimplemented in zig).
        
       | davidkunz wrote:
       | I'm a bit concerned when library authors only test it with
       | blocking Io and the consuming app with a different kind. Wouldn't
       | this potentially lead to bugs?
        
       ___________________________________________________________________
       (page generated 2025-07-13 23:01 UTC)