Live data from Hacker News

What color is your function? (2015)

journal.stuffwithstuff.com

51–60 of 63 posts

Re: What color is your function? (2015)

#51
post #28

The different philosophies of "explicitly annotate async functions" vs "implicit async" vs "create os threads with polling" etc will always continue because they all have tradeoffs. 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 has solved the color problem so it's not an issue." -- the reflexive skepticism…

There are definitely tradeoffs. In javascript I can cavalierly code knowing that a sequence of lines will not be interrupted by another thread. (Unfortunately, I still have some mental overhead because I keep thinking, "This program will be screwed if they ever introduce multi-threading".)

Re: What color is your function? (2015)

#52
I wish there were a statically typed language with Lua's stackful coroutines and the ability to manually yield. It seems that Lua and Ruby are the only two well-known languages with that kind of stackful coroutine. And also, LuaJIT has proven that they can be implemented in a performant way, so I'm not sure if performance is a valid obstacle to implementing them more often.

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.

Re: What color is your function? (2015)

#53
Scala got a really nice solution to this whole problem area lately.

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.

Re: What color is your function? (2015)

#54
post #7

Zig’s functions are colorblind: https://kristoff.it/blog/zig-colorblind-async-await/

Very nice, but how does it support separate compilation? Does the compiler always compile two versions of each function or zig only support whole program compilation ( by JITing for example)?

Or is it just stackful coroutines and there is really no difference between async and sync functions?

Re: What color is your function? (2015)

#55

Earlier quoted context omitted.

I've been burned by this so many times that I just simply will never use async in any code that is beyond trivial complexity and/or has performance requirements attached. I'll recommend breaking your solution into small pieces that execute separately and pass data to one another via messaging, to isolate the "colors" from one another at the process level.

I'm not a huge fan of async/await (though I have to write a fair amount of it) but that's throwing the baby out with the bathwater. The way to avoid "sync-over-async" deadlocks is to not do sync-over-async, i.e. don't call blocking methods/properties like Wait() and Result on tasks from threadpool threads - it's not what they're for (though they have valid, necessary uses and therefore can't simply be removed). I've…

> more generally not thinking clearly about what their multithreaded code will actually do when it runs

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.

Re: What color is your function? (2015)

#56
post #23

You can call an async from a non async function of course. You just can’t return it’s result synchronously. 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 i…

The issue is if you need to add an async call to an existing synchronous code base. If you introduce async in this situation, you’ll probably need to rewrite a huge chunk of your code to allow it. To prevent inconsistencies, this leads to a practice where programmers return promises or tasks by default, even in synchronous functions; which can cause a myriad of problems beyond simply looking really ugly.

In Rust, calling an async function from a sync function looks like this[0]:

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

Re: What color is your function? (2015)

#57
post #7

Zig’s functions are colorblind: https://kristoff.it/blog/zig-colorblind-async-await/

Very nice, but how does it support separate compilation? Does the compiler always compile two versions of each function or zig only support whole program compilation ( by JITing for example)? Or is it just stackful coroutines and there is really no difference between async and sync functions?

I believe it's up to the programmer to implement this. Here's some code from the zig github[1] page that illustrates how it might look once zig is stable (this is from an issue).

  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/1778

Re: What color is your function? (2015)

#58
post #38

Earlier quoted context omitted.

> 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. 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 men…

>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. 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),…

I don't think there is really a "better" option than this. Personally I find it distasteful because (if I understand this feature correctly) it means I have to know whether somebody turned on the async flag in another file to fully understand the possible control flow of a function I'm reading. This is different from Erlang because we just assume that every line can be preempted, and different from async-await because seeing async-await means you don't know if the code is actually going to run asynchronously. It's not the choice I would have made, but I am a stickler for theoretical cohesiveness and local readability, and Zig is kind of an anti-theory language. I would have chosen function colors, in all likelihood.

Re: What color is your function? (2015)

#59
post #57

Earlier quoted context omitted.

Very nice, but how does it support separate compilation? Does the compiler always compile two versions of each function or zig only support whole program compilation ( by JITing for example)? Or is it just stackful coroutines and there is really no difference between async and sync functions?

I believe it's up to the programmer to implement this. Here's some code from the zig github[1] page that illustrates how it might look once zig is stable (this is from an issue). 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…

Thanks, but how is a waiter suspended? The typical await implementation will do a CPS conversion of the waiter function, but to trigger the conversion it needs to see an await call, or pessimistically generate two versions of each functions.

But 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?

Re: What color is your function? (2015)

#60
post #35
post #32

Earlier quoted context omitted.

This is interesting, it seems like a global mode that kind of turns normal Zig code into Go or Erlang-like non-blocking code. It does mean you have to hold non-local context in your head while reading functions, but maybe that's a worthy tradeoff?

> This is interesting, it seems like a global mode that kind of turns normal Zig code into Go or Erlang-like non-blocking code 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…

what happens if there are multiple conflicting settings of io_mode (does Zig have the concept of translation unit?).
Post reply on HN