Or is it just stackful coroutines and there is really no difference between async and sync functions?
pub fn write(file: *File, bytes: []const u8) usize {
// Note that this is an if with comptime-known condition.
if (std.event.loop.instance) |event_loop| {
// non blocking version
} else {
// blocking call
}
}
So I guess functions that don't support async could just do something like: if (!std.event.loop.instance) @compileError("X function only supports async");
[1]: https://github.com/ziglang/zig/issues/1778But from your example it seems that Zig just do context switching (not knocking it, I'm a big fan of it over await), but then why would it need an await/async keyword at all?
Nope! There's no language magic going on, and the global switch is just a global variable in the root module. It only affects a few functions in the standard library that check that variable.
Zig relies heavily on compile time code executing for metaprogramming, and your code can read symbols from the root source file via @import("root"). All that
pub const io_mode = .evented;
does is set a variable that the standard library then checks. When io_mode is set to .evented, the library sets up an event loop and makes async calls internally. When it isn't, it makes blocking calls as you would expect. Unless you explicitly use 'async' to call functions, then they're going to act as if they block as they will be immediately await-ed upon, so there's no change.I didn’t say there was magic, I was describing the practical effect of this feature. On a type-theoretic level, it’s like entering a global async monad, which is roughly the structure that Go and Erlang have.
But since you mentioned it, global variables that have side effects are definitely magic, in my view. If I did this in Ruby by monkey-patching synchronous IO methods in response to a global flag, people would definitely call it magic. And I understand how the feature works, but my reservations about it remain.
As for results being immediately awaited meaning there’s no change, I think I disagree. What happens if another task fails and brings down the whole system while I’m awaiting something? It wouldn’t have been scheduled in if I was programming synchronously.
It definitely is "magic", but it's one of the few areas where I welcome it, personally.
What other options are available? The three I know of are function-colored (JavaScript, Python), async/automatic context-switching by default (Go, Erlang), or monkeypatching to go from colored to async/automatic context-switching by default (Zig, Python gevent).
I also hate globals, side effects, and monkeypatches, but I find the monkeypatch method to be the best compromise. I still use gevent in pretty much every Python project, and have for the past 10 years.
This is the most concise summary of what Zig actually does! Thank you.
I always felt it was a little off, but it makes sense when you think about it in terms of "Zig makes all of your functions red, even if you don't want them to be."
In fact, in the article linked above, it says this in the FAQ:
> Q: SO I DON’T EVEN HAVE TO THINK ABOUT NORMAL FUNCTIONS VS COROUTINES IN MY LIBRARY?
> No, occasionally you will have to. As an example, if you’re allowing your users to pass to your library function pointers at runtime, you will need to make sure to use the right calling convention based on whether the function is async or not. You normally don’t have to think about it because the compiler is able to do the work for you at compile-time, but that can’t happen for runtime-known values.
So in essence, Zig actually is still colored, but the compiler handles for you what it can.
I suppose user code can do the same thing, but still, it's kind of magic.
When people speak of magic in code, they mean something that looks innocent but has huge ramifications because some other part of the code (that you are not aware of) does special handling.
In this case, what looks like a simple variable declaration acts more like configuration, but it doesn't look like configuration.
It would be much less magical if instead the language required you to call something from main, for example:
fn main() {
std.setup_event_io();
....
}That's almost inevitable given its desire for close interop with C (as in, the Zig compiler includes a C compiler and can directly #include C headers).
The standard library has a relatively consistent case style though, which the Zig guide recommends.