6 ms·
If you want to go down that route, any function that has, or doesn't have, any given resource is colored then. fn foo(db: *Db) !void { ... } fn bar() !vo
by kristoff_it 1y ago
If you want to go down that route, any function that has, or doesn't have, any given resource is colored then.
fn foo(db: *Db) !void { ... }
fn bar() !void { ... }
Would you consider `foo` a blue function and `bar` a red function? That doesn't seem particularly helpful to me.
The virality of async await is that once you mark a function async, then you can only call it from another async function, which forces you to mark more functions async, which in turn means that if you want to use blocking I/O APIs then you just can't because it's incompatible with your execution model because by daring to express asynchronicity of operations, you were forcefully opted into stackless coroutines.
That's what Zig solves, and that's what is real function coloring. People have written reimplementations of the same libraries multiple times because of it.
https://github.com/redis/redis-py https://github.com/redis/redis-py
https://github.com/jonathanslenders/asyncio-redis https://github.com/jonathanslenders/asyncio-redis
Just as an example. Note also how, coincidentally, this duplication of effort resulted in asyncio-redis being semi-abandoned and looking for maintainers. And you have to have both libraries because the asyncio one can't do blocking, and vice versa the other one can't do async.
Would you write two instances of essentially the same library just because one is missing an argument that gives it access to an `Io` interface? No, because you would just pass that extra argument around and nothing else would have to change.
- n42 1y agoI've been trying to beat this point in and failing. If a parameter type creates "colors", you can extrapolate that to an infinite set of colors in every single language and every single standard library, and the discussion on colors becomes meaningless. Some people are so focused on categorical thinking that they are missing the forest for the trees. The colors are a means of describing an observed outcome -- in Node's case, callback hell, in Rust's, 4 different standard libraries. Whatever it may be, the point is not that there are colors, it's the impact on there being colors. > But there is a catch: with this new I/O approach it is impossible to write to a file without std.Io! This sentence just makes me laugh, like it's some kind of "gotcha". It is the ENTIRE BASIS of the design!
- deleted 1y ago[deleted]
- dwattttt 1y ago> you can extrapolate that to an infinite set of colors in every single language and every single standard library, and the discussion on colors becomes meaningless. It's more that discussion about most of them becomes meaningless, because they're trivial. We only care when it's hard to swap between "colours", so e.g. making it easy to call an Io function from a non-Io function "removes" the colouring problem.
- hamandcheese 1y ago> so e.g. making it easy to call an Io function from a non-Io function "removes" the colouring problem. Exactly. In golang (which is also a cooperatively multithreaded runtime if I understand correctly), calling a function that needs IO does not infect the callers type signature. In async rust, and in "async param" zig, it does.
- naasking 1y agoAnother poster up thread identified the exact problem: async/await contexts are not first-class values, they are second class citizens. If they were values then you could just stick the context in a struct/class and pass that around instead, and avoid having to refactor call chains every time something changes. It's their second class status that forces the "colouring" into the function signature itself at each point. This is also why ordinary first class values do not introduce colours, ie. you can hide new values/parameters inside other types that are already part of the function signature, thus halting the propagation/vitality of the change. Of course, if these async contexts were first class citizens then you've basically just reinvented delimited continuations, and that introduces complications that compiler writers want to avoid, which is why async/await are second citizens.
- SkiFire13 1y ago> async/await contexts are not first-class values Because async/await are not values, they are ways to run/structure your code. That's why they are so infectious. If you don't want the division the only solution is to make everything of one kind. Languages like C make everything sync/blocking, while languages like Go make everything async.
- woodruffw 1y ago> Would you consider `foo` a blue function and `bar` a red function? That doesn't seem particularly helpful to me. In the sense of effect/capability typing, I think the answer is yes. "Coloring" isn't magical, it's just a way to describe effects. Those effect can be described by keywords (`async` in JS, `throws` in Java, etc.) or special token parameters/types (what Zig does), but the consequences are the same: the effect propagates to the caller, and the caller becomes responsible for dealing with it.
- nextaccountic 1y agoYeah, parameter passing can be seen as some kind of effect (like the Reader monad in Haskell). Passing parameters down the call stack is viral just like function coloring (if you need to do i/o and thus need to receive an io parameter, then your caller must receive an io parameter too, recursively; this is analogous to adding a keyword async in front of your function, and to its callers recursively). The solution is probably some sort of implicit parameter, again, like the Reader monad. Note: async fn in Rust is also just a fn (in Rust's case, a fn that returns a future). It turns out that returning a future, or receiving as parameter a reference to the runtime, are equivalent in some sense
- koiueo 1y ago> Passing parameters down the call stack is viral It's in a different league comparing to async-await abomination. For one, you can store parameter on a struct thus working around the "virality" on a call site.
- 0x457 1y agoHuh? You have the following call stack (top to bottom): a -> b -> c -> d Function "d" does IO, so a, b and c need to pass that IO down. It's the same as with async in terms of virality. If I understood correctly, the main benefit here is that you don't have to dance around "can I call this function or is it going to block async runtime reactor?" I would pick async/await style over this because (I think) extra function argument is higher visual load than "async fn".
- tcfhgj 1y ago> The virality of async await is that once you mark a function async, then you can only call it from another async function Rust calling async function in non-async function: ... // Create the runtime let rt = Runtime::new().unwrap(); // Get a handle from this runtime let handle = rt.handle(); // Execute the future, blocking the current thread until completion handle.block_on(async { println!("hello"); }); https://docs.rs/tokio/latest/tokio/runtime/struct.Handle.html#method.block_on https://docs.rs/tokio/latest/tokio/runtime/struct.Handle.htm...
- vlovich123 1y agoOf course, spinning up a new runtime within the context of a boundary like that is probably wasteful (lots of new threads created if you’re not careful). But you could stash that runtime behind a OnceLock (you’d need to block_on the Handle I imagine rather than the Runtime directly, but doable). And calling blocking from non-blocking: let result = tokio::task::spawn_blocking(|| { 5 }).await; This of course is basically essentially what Zig is doing, except instead of hidden global state it’s parameter passed. This is one area Zig does do better in - I wish Rust would default more to instance state instead of implicit global state.
- Dylan16807 1y agoSo that's an escape in Rust and tokio. But consider a language that doesn't have block_on, like javascript.
- rk06 1y agoWell said. Honestly I don't understand this "function coloring argument" at all in zig's context. Function is the smallest unit of logic. And function parameter is the fundamental way to control it.so, in theory, __A function parameter is the smallest possible design choice you have to control async vs sync__ There is no way to reduce it further. As you need to encode this somehow, otherwise it will be implicit and very hard to debug. functions can compose. Function parameters can be composed. Making this a solid design choice. Zig has somehow achieved this feat of decomposing async/sync to a single parameter. This deserves an ovation Other langs should take note.
- metaltyphoon 1y ago> in theory, __A function parameter is the smallest possible design choice you have to control async vs sync__ So we are just going to forget Go exists?
- rk06 1y agoDisclaimer: I have not used golang, so i might be wrong. as per my understanding, golang has it implicit. Like there is no way for a developer to tell if code will run under async context or not.
- throwawaymaths 1y agono, in golang (and erlang) every function is async, and there are implicit yield points in "well-known" places. for erlang it's every function call boundary, for golang its the end of functions. in the past this was a problem for go because a for (ever) loop could block your cpu process. not a problem for erlang since all forever loops must be tco function calls (inducing a yield at each interation)
- alex_hirner 1y ago> The virality of async await is that once you mark a function async, then you can only call it from another async function That's not exactly true. Many languages return a curried version of it, which can be executed once you have a runtime.
- ivanjermakov 1y ago> No, because you would just pass that extra argument around and nothing else would have to change. > Semantically, passing std.Io to every function is no different from making every Node.js function async and returning a promise. Zig shifts function coloring from blocking/non-blocking choice to io/non-io. > Complains are about how inconvenient it is to work with [blocking and non-blocking function handling] differences "Extra argument" in this case is not much different from "extra async keyword", because you can only run I/O functions when having std.Io in scope.
- Dylan16807 1y agoThey're similar, but if you're not writing Haskell it's super easy to make a variable always be in scope. You don't have to change all the intermediate functions to do it.