(This is a section of the lilos intro guide that people seemed to like, so
to increase its visibility, I’m lifting it up into its own post and expanding it
a bit. I hope this is a useful companion piece to the post on async
debugging I posted this morning.))
Some documentation of Rust async and await has presented it as a seamless
alternative to threads. Just sprinkle these keywords through your code and get
concurrency that scales better! I think this is very misleading. An async fn
is a different thing from a normal Rust fn, and you need to think about
different things to write correct code in each case.
This post presents a different way of looking at async that I think is more
useful, and less likely to lead to cancellation-related bugs.
(I’ve updated this pattern, since a lot has changed since 2020. The
recommendations here should be ready for the Rust 2024 edition, and are closer
to correct in a post-pointer-provenance world.)
Here’s another useful Rust pattern. Like the Typestate Pattern
before it, I wrote this because I haven’t seen the sort of obsessively nerdy
writeup that I wanted to read. And, as with the Typestate Pattern, I didn’t
invent this — I’m merely documenting and generalizing it.
The typestate pattern is an API design pattern that encodes information about
an object’s run-time state in its compile-time type. In particular, an API
using the typestate pattern will have:
Operations on an object (such as methods or functions) that are only available
when the object is in certain states,
A way of encoding these states at the type level, such that attempts to use
the operations in the wrong state fail to compile,
State transition operations (methods or functions) that change the
type-level state of objects in addition to, or instead of, changing run-time
dynamic state, such that the operations in the previous state are no longer
possible.
This is useful because:
It moves certain types of errors from run-time to compile-time, giving
programmers faster feedback.
It interacts nicely with IDEs, which can avoid suggesting operations that are
illegal in a certain state.
It can eliminate run-time checks, making code faster/smaller.
This pattern is so easy in Rust that it’s almost obvious, to the point that
you may have already written code that uses it, perhaps without realizing it.
Interestingly, it’s very difficult to implement in most other programming
languages — most of them fail to satisfy items number 2 and/or 3 above.
I haven’t seen a detailed examination of the nuances of this pattern, so here’s
my contribution.