Hacker News new | ask | show | jobs
by naasking 333 days ago
Another 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.

1 comments

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

> Because async/await are not values

They are computations that produce values. Computations can be reified as values. What do you think functions and threads are?

As I described above, "delimited continuations" are values that subsume async/await and many other kinds of effects. You can handle async/await like any other value if they were reified as delimited continuations, but this makes the compiler writer's life much more difficult.

> As I described above, "delimited continuations" are values that subsume async/await and many other kinds of effects.

Supporting delimited continuations force some specific ways of performing computations. They are akin to making everything async, which proves my point: you have to make everything of one kind in order to solve the problem.

> They are akin to making everything async, which proves my point: you have to make everything of one kind in order to solve the problem.

You can use ordinary direct style compilation, but all references to stack values simply have to be relative offsets, then a simple implementation of shift/reset is just capturing context and copying stack fragments, which you can do using setjmp/longjmp in C (although there are better ways [1]).

This is not akin to making everything async, nor is everything "of one kind", whatever that means. A delimited continuation is very much its own kind of thing, distinct from other values, and doesn't have to influence the function call/return semantics unless you're targeting a less flexible runtime like the JVM.

[1] https://github.com/koka-lang/libhandle

> but all references to stack values simply have to be relative offsets

Then such references are no longer pointers, and in order to have a generic reference (that can point to both stack memory and heap memory) you have to store additional data (which one of them it points to) and conditionally use the correct one any time you access it. This is a very invasive change to the memory model.

> then a simple implementation of shift/reset is just capturing context and copying stack fragments, which you can do using setjmp/longjmp in C

That sounds like you're just reinventing green threads then, which basically forces everything to be async once you start noticing its issues.

> https://github.com/koka-lang/libhandle

This returns a 404 error for me.

> This is not akin to making everything async, nor is everything "of one kind", whatever that means.

Sure, if everything has to be able to "wait" for the result of a continuation then you're forcing async support on everything.

If instead you implement continuations as some kind of monads that users have to return from their functions then you got function coloring because you can't normally wait for the result of a continuation from a function that does not return a continuation.

> https://github.com/koka-lang/libhandle

Dropped the r somehow:

https://github.com/koka-lang/libhandler

> Sure, if everything has to be able to "wait" for the result of a continuation then you're forcing async support on everything.

If "forcing async support" doesn't mean that code has to change, or any other code in the call chain, then it's just meaningless pedantry.

> the only solution is to make everything of one kind

That's not really the case. All you really need is a way to run async code from a sync function; "keep doing this async thing until it's done" is the primitive you need, and some languages/runtimes offer this.

Going by that reasoning then Rust solved the colored function problem too: when calling an `async` function from a sync one use `block_on`, while when calling a sync blocking function from an `async` one use `spawn_blocking`, but somehow people are not happy with this.
Yes, it's quite curious. Having used both block_on and spawn_blocking, and not being worried about what "colour" my function is, I am also quite confused about the fuss.

On a practical note, since Rust doesn't standardise on an async runtime, it would be more accurate to say tokio solved the coloured function problem, for whatever that means. Or any and everyone else that made it easy to call one coloured function from another.