What color is your function? (2015)
journal.stuffwithstuff.com
journal.stuffwithstuff.com
What Color Is Your Function? (2015) - https://news.ycombinator.com/item?id=23218782 - May 2020 (85 comments)
What Color is Your Function? (2015) - https://news.ycombinator.com/item?id=16732948 - April 2018 (45 comments)
What Color Is Your Function? - https://news.ycombinator.com/item?id=8984648 - Feb 2015 (143 comments)
What Color Is Your Function? - https://news.ycombinator.com/item?id=8982494 - Feb 2015 (3 comments)
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.
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();
....
}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?
The way suspend functions work in Kotlin is that it is essentially syntactic sugar for async await where calling a suspend function implicitly awaits it; which you can only do in a so-called CoroutineScope (for example another suspend function).
It's actually very similar to what go does. Instead of go foo(), Kotlin makes you write something like CoroutineScope(CoroutineName("myscopename")).launch { foo() }.
It's actually very similar to what go does as well. Instead of go foo(), Kotlin makes you write something like CoroutineScope(CoroutineName("myscopename")).launch { foo() }
The difference is that a coroutine scope is a thing with a name and some behavior. For example, you can control how the coroutine is dispatched for example. Some dispatchers might do everything on the main thread. Other dispatchers use thread pools. Very useful if you are integrating some blocking IO framework where you don't want to block the main thread.
There's more to it but it's a pretty well designed solution for what is a tough problem space.
Nitpick aside, I have got to the point where I care little! Every language has its boilerplatish things, be it Elm’s JSON parsing, GO’s “if (err != nil)” or Haskells myriad language extension declarations. I don’t think asyncs on a chain of functions, or converting a callback function into a promise based one is that bad.
What it seems is needed is a blue function that wraps a red function with (in the simple case, though the actual inplementation needs to be more complicated to identify when thr sinple case applies vs when you have to do something more complicated) a dedicated single-item event loop and returns its result [0] (in async-await, I’d want to have a keyword “sync” used much like “await” but in an otherwise synchronous context to wrap a red function in this blue function.) JS—or at least browsers specifically—might avoid having this, or the tools to do it, to avoid encouraging use of async constructs in a sync context which could kill UI liveness, but for other red/blue async/sync environments it would be useful to have, whether in function or keyword form.
[0] “async_to_sync" in this article is an example (for Python, but the general idea would be the same un any environment): https://www.aeracode.org/2018/02/19/python-async-simplified/
block_on(myasyncfn())
This basically repeatedly polls the future returned by the async function, until it completes, and then returns it. (you can also call a full-featured executor like Tokio, but for simple stuff block_on is perfect)My mental model is like this: just like you use await to wait for a future in an async context, you use block_on to wait for a future in a sync context.
You only need to call block_on on the top-level future, meaning that myasyncfn() can have a lot of futures underneath. The only thing that won't work is spawning other tasks (new top-level futures that can continue executing even after the future returned by the function completes). Which is actually amazing: after block_on returns, there is no async background tasks lurking on: the program just continues as a normal non-async program.
I don't know how async is in other languages, but I guess that every language has something equivalent to block_on? How on Earth you integrate async code into sync code otherwise?
As an aside, calling a potentially blocking sync function from an async function is easy too, like this[1]:
spawn_blocking(myblockingsyncfn)
This spawns a new OS thread (or reuse one from the threadpool dedicated to blocking operations), then run the provided function in there. spawn_blocking returns a future that, when awaited, waits for the function in the other thread to complete, and then returns its value.[0] https://docs.rs/futures/0.3.17/futures/executor/fn.block_on....
[1] here the exact function to call depends on the executor you use to run the event loop of your async program, but all major ones provides a spawn_blocking API, like https://docs.rs/tokio/1.12.0/tokio/task/fn.spawn_blocking.ht... and https://docs.rs/async-std/1.10.0/async_std/task/fn.spawn_blo...
result = asyncio.run_until_complete(func)
Also, having explicit async/await are a wait to tell you: here are the point in your code that have something doing unreliable side effects down there.You can then use this information to:
- move that code and group it, so you get the rest of the code sansIO style
- put that in a try/except, because this code WILL fail at some point
- think about the perfs of this part of your code, which is what you are doing async io the first place
I do wish that python coro would schedule themself automatically, like JS does, and return a future that you can await explicitly. But that ship as sailed.
I've never written code that's deadlocked in this way. I've fixed a ton of such deadlocks, though, and every one boils down to somebody not following this simple rule, and more generally not thinking clearly about what their multithreaded code will actually do when it runs. (Encouraging people to be sloppy in their thinking about these issues is one of the reasons I mildly dislike C#'s concurrency facilities - but nonetheless it's perfectly possible to write code with them that isn't riddled with deadlocks.)
The whole reason people are trying to build better concurrent/parallel computational abstractions is because they're sick of having to think about that stuff. But to be able to stop thinking about it you need to arrive at abstractions that don't leak: that is, their failure modes should themselves be (non-leaky) abstractions of the lower-level error details. Async/await still forces you to learn about thread-pools and scheduling — while in Erlang one has to learn about scheduling only to optimize performance, and deadlocking requires one to explicitly write a "receive" without a timeout.
So, the "simplification" or a "unification" of a concurrency model always comes at a cost that some programmers don't want to pay. Whenever you see, "Language <X> has solved the color problem so it's not an issue." -- the reflexive skepticism should be, "At what price, and do I want to pay it?"
I think it's instructive that Rust had a clean-sheet start to design a new language and so far, they've rejected Go style "channels" as Rust core syntax and instead, adopted the async/await paradigm. You don't have to program in Rust to get the gist of the following previous comments about their thought process:
https://news.ycombinator.com/item?id=21475154
https://lobste.rs/s/bfsxsl/ocaml_4_03_will_if_all_goes_well_...
https://lobste.rs/s/y3fsrm/what_is_zig_s_colorblind_async_aw...
Progress, I guess.
Its new "cps-async" implementation has "auto coloring" as a highlight feature. The Scala compile will just do the right thing in an async context.
Details can be found here:
https://rssh.github.io/dotty-cps-async/
One of the more interesting parts is also that "cps-async" works fine with arbitrary effect monads. There are integrations with Cats' IO and ZIO and some more.
Even languages like Go and Java with proper fibers don't allow you to yield where you want to, and the yield points are inserted by the compiler/runtime instead.
A use case which I can't seem to reimplement without stackful coroutines is "inline" blocking input, as found in the source code of old roguelikes. It makes the game logic easier to follow since you don't have to rearchitect the entire engine to be event-based, and can instead yield between an "update" and "draw" coroutine each frame.
msg("Do you want your possessions identified?)
local yes = yes_or_no()
if yes then
identify_possessions()
end
I was honestly very surprised when I first realized that this is not how most graphical turn-based games are programmed.Among the statically compiled languages, there are a few options in C++ for stackful coroutines, for example boost.context.
There is still a chance that they might make it into the standard at some point.
Are these two fundamentally opposing viewpoints? Or can these two sets of ideas be reconciled somehow?
The article that I posted specifically calls out Java as an example of why you need explicit await, yet the article in the OP praises Java for not having it!
I would be really keen on seeing a mainstream-ish language that does algebraic effects or that can even implement them in a legitimate sense (discounting React’s implementation here, for instance). Purescript tried, but are doing something different these days. https://purescript-resources.readthedocs.io/en/latest/eff-to...
I am by no means an expert in this area, but I hope this clarifies the intent of my original comment. What transformations and/or constraints exist for composable heterogeneous effects?
Koka: http://koka-lang.org/
I'm afraid I just don't quite understand why async/await don't solve the problem and instead sweep it under the rug, could someone explain it?
No, that is wrong in multiple ways. Javascript is fundamentally designed around an event loop processed by a single thread. The only multitreading facility it has are web workers, which don't share memory with the main thread.
> I'm afraid I just don't quite understand why async/await don't solve the problem and instead sweep it under the rug, could someone explain it?
Both are really only syntactic sugar over callbacks. Under the hood, async/await splits the await-ing function and passes the second part as a callback to the await-ed function. And it requires the await-ing function itself to be async.
Promises are just a nicer way to do callbacks. It's impossible to get a result from a promise (or an ansync function) synchronously. If there were a way, it would block the main thread and kill performance completely.
It's especially important not to post unsubstantive stuff to a fresh thread, because threads are super sensitive to initial conditions.
This result is a Promise, that you can pass sync callbacks to, and keep the whole code away from async/await.
You can also decide that async/await is nice, and just use that instead.
I'm a Python dev, but that part is better in JS.