Hacker News new | ask | show | jobs
by bourneavent 1109 days ago
>How does "func EatADonut() async {}" aka "eating a donut is an inherently async action" even make sense to people?

You have to think of it from a higher level.

Nobody knows if eatdonut is strictly async but everyone knows all IO calls are async. So something like socket.send would be async, this is obvious.

Then anything that calls socket.send would then in turn become async itself. The async sort of spreads around like a virus. Any function that uses other async functions gets polluted with the async marker.

So in this sense, you should think of it like this. If eatADonut is async it means it touches IO. It sends data somewhere else to some other thing. It also means it's not unit testable.

In a nut shell This is the intuition you should have about async:

   Async Tells You about the properties of eatADonut. eatADonut doesn't tell you anything about whether or not it's async.
This is largely identical to how the IO monad works in Haskell. Unless you want all your logic polluted with the IO monadic wrapper or the async marker you have to learn to segregate your functions into async functions and non-async functions.

Under this intuition, EatADonut, by good design principles should NOT be async. This is what it should be:

    fn eatAdonut() -> ResultOfEatADonut
    async fn sendDataToIO(data: Data) -> ()

    async parentCaller() -> () {
       let data = eatAdonut();
       sendDataToIO(data.serialize()).await;
       ()
    }
The async marker forces you to organize your code in this way, You have to do this unless you want all your functions to be polluted by the async marker. This is the proper way to organize code Anyway, as it allows your code to be unit testable. Async calls are, in general, not unit testable because the functions are not pure. So if you look at what I did above, I segregated eatAdonut Away from IO and made part of the logic of the overall code testable via unit tests.

IO calls should be very general and small functions while your other functions should be pure and unit testable. This is the overall intuition you should have about async.

Believe it or not, this specific aspect of async is actually an improvement over golang style concurrency. Not an overall improvement, it's still debatable which style is better, but I'm saying the async marker is actually a feature that other styles of implementing concurrency don't offer.

Basically, async encodes the concept of IO and function purity into the type system. It allows the type checker and You to tell which functions touch IO and which functions are pure and unit testable.

People think unit testability is some kind of marginal benefit because you can cover testing with integration tests which are basically tests that happen across IO. It's more than that. The ultimate dream of programming something as if it's a bunch of modular lego blocks that you can elegantly place together is most fully realized with pure functions. Async enables more of this style of modularity in Rust (but not without some cost which is why it's debatable whether or not it's better than go style concurrency).

1 comments

> but everyone knows all IO calls are async

Do you mean on a hardware level? Because otherwise, hell no.

If I’m writing a binary file parser that buffers some data and processes it, I sure as hell want to wait for IO, there is no other meaningful way forward in most cases. It is also not a small isolated part, but a tight loop bouncing between CPU and IO.

Async is a function of the caller.

>Do you mean on a hardware level? Because otherwise, hell no.

I mean in the context of async await. The smallest primitive that is properly async is an IO function.