(Music)
This is Self-Directed Research.
James and Amos are back with weekly
hyper-focused deep dives.
Make sure to like, follow, and subscribe
wherever you find us.
And as always, visit SDR-Podcast.com
slash episodes for all the presentations,
videos, show notes, and transcripts.
This episode is sponsored by Depot.
Stay tuned at the end
for more information.
James tried something new by recording
first and adding slides later.
This week's episode is Sans I.O.
Meh.
(Music)
So I don't have anything to follow.
Cool.
All right.
Bye.
Yeah, no slides for this one.
This one's going to be a time travel
episodes because I'm going to
write the slides afterwards.
But I had a thing that I used to
recommend to people.
And now after doing a bunch of work, I
found my opinion changing.
So I wanted to see almost what your
opinions were or if I'm missing something
as my opinion shifts.
So-- James, did you
already retire the buddy system?
No.
Scared me for a second.
No.
So have you heard the
phrase Sans I.O. before?
I have.
So this is one of those things that,
especially in Rust, people tend to
recommend of like, hey, when I'm
designing protocols or things like that,
one of the recommended ways is Sans I.O.
So you have all of your code that does
decoding and state machine logic and
stuff like that in normal,
usually blocking functions.
And you handle your actual
I.O. portions outside of that.
And the purported benefits are it's
generally portable for async and not
async because you let the I.O. be I.O.
wherever it's doing I.O.
You get benefits of testability because
when you want to test it, you can just
shove in VEX or slices of bytes that are
pre-manufactured test artifacts and make
sure it goes only to the
next step and things like that.
It is sort of compelling.
But what I've realized is the whole point
of async is to make the
compiler do state machines for you.
And to not have weird like loop match
statements where you're like doing manual
state machine changes.
Someone even in the Rust subreddit today
posted something where
they're like, "Can I do this?"
And they posted some loop match.
And I'm like, "If you just use a regular
async function, you don't need the match
like it's just a for loop with a continue
statement in the middle if you need to
skip back to the first one."
And I've realized particularly with
postcard RPC and post station that if you
do have a good abstraction of the wire,
you can avoid losing out.
So the reason, the negative to doing this
is it becomes difficult because you are
really doing those match
of state machines again.
You go, "Okay, decode the packet.
What does this packet make me do?
It makes me move this state."
And a lot of people reach for enums for
state machines where you
go like match self.state.
And that works fine except for everyone
wants to put data inside of those because
you say, "When I'm in
this mode, I have this data."
And it helps you make sure that you don't
have data that gets stale when you're in
the wrong state and things like that.
And for a lot of these where you might
wanna borrow the contents of those
states, then going to like swap the state
out while you're in the middle of a match
statement for doing this, one, you get
really nested code because
you're in a match statement.
And even if you delegate to functions,
you have to figure out, "Oh, well I'm
already mutably borrowing self.state, so
I can't just pass state to this function
that also mutates self because I'm
already borrowing self.
And if I take the thing out, then I need
a placeholder value, which means what do
I do in my state machine?
If I'm in the placeholder
value, I have to just panic.
So you have a panicking branch in there.
So it's one of those things where it's,
it sounds really good on paper.
You can really enumerate the benefits,
but then it's kind of every time I've
tried to really do it like
that, it works really well.
And then the paper
cuts just keep lining up.
That is true.
Versus really leveraging something like
async where a function is a
hierarchical state machine.
Like if you have one
function, that is a state block.
And if you call another function, that's
another state block.
When I say hierarchical state machines,
do you know what I mean by that?
I do, I do.
You didn't really leave me time to
interject, but you know
what's very fun, James?
I'm excited about this topic, Amos.
You know what's very, very fun, James, is
that I have my shit together in 2025.
That's also an explicit
episode, I'm sorry, kids.
Hell yeah.
I have my shit together, which means I
have a video backlog.
Do you wanna know the name of video I've
already researched, written, shot,
edited, and captioned?
Why Sanzio isn't great.
The case for Sanzio.
Ooh, point counterpoint.
I maintain a crate named RCzip, which is
a Sanzio implementation
of the zip file format.
And that's the topic of the video, and I
explain why it's actually good.
So I can make the same points in podcast
form, because I think, realistically
speaking, the overlay
will be relatively small.
And I'm gonna actually get to bump it off
of you instead of just
talking to my shotgun mic.
But it's very funny, because I knew this
day would come eventually.
And actually, so you say zip file.
So actually, I think I am much more
inclined to agree with things like file
format parsers or things that are not
distributed systems.
Because I think a lot of protocol design
when it comes to distributed systems is
your action and reaction to things.
And surprisingly, there's not as much
difference as you would
think between a file format.
A file format is just kind of like a
fixed distributed system.
It's something that you're talking to and
decoding and things like that,
but-- Well, you will let me know if you
still think that after you, of course,
watch my video about the zip file format.
Because it, well, it's static, yes.
But going from left to
right, no, very much not.
So it's more complicated than TLS even.
But okay, I'll throw one
first argument in the ring.
No pun intended.
It's that the async abstractions we have
now, async retrades in the Tokyo crates
and in the async-sad
crates are not actually good.
And also just not compatible with modern
ways of doing IO, which for me on Linux
and BSDs and now on Windows and whatnot
would be IO-uring, where you would give
ownership of the buffer to the kernel and
then wait for the opportunity to be
completed or canceled before you can
actually free that buffer
or do anything else with it.
And you simply cannot have Tokyo's
async-rein interface and an IO-uring IO
system, which is the most efficient way
of doing IO nowadays.
You simply cannot.
So of course, immediately after that, the
argument goes, you don't need IO-uring
because your application is too slow.
That's not the bottleneck essentially.
So that's a good point.
Facebook needs
IO-uring, you probably don't.
I don't think that's
where I would go with that.
I would say I probably agree with you
that the abstraction isn't great because,
so what Tokyo, especially when you're
talking about like TCP streams and things
like that, you get a stream of data as
bytes and you have to do explicit steps.
So this is one of those things that I've
boiled down in postcard.
Postcard RPCs, wire traits.
So it has wire traits for
both the client and the server.
They don't give you a stream of bytes,
they give you a frame.
And I do, and it depends on whether
you're on no standard or not, but on
desktop, it usually is you give evacuate
as a frame and you receive back evacuate
as a frame, not decoded, but you say this
is one block where I expect framing to be
an explicit step of this.
Now you could probably do something
clever where you have more of like a
buffer pool if you didn't really wanna
recycle VEX, but for my
case, I'm not Facebook.
So for me, leaning on Malek or TC Malek
or JE Malek is probably fine and the VEX
are gonna get reused anyway, but if you
really wanted control over it, you could
have more of like a recyclable buffer
pool or something like that.
But my actual interface is at the frame
level because one, I need to be flexible
because if you're going over a serial
port, you might need to do some
additional encoding or putting checksums
on it because you may not
trust that that's going.
Whereas over, if you decided to use UDP
as your transport, you might just say,
well, the frame's a frame or you might
wanna do it with encryption.
So I have a version that goes over TLS
1.3 using a Russell's.
And so you can choose
where you delineate the frame.
And so frames come in, then there's an
explicit decoding step.
So you turn it from a frame to a struct
or it fails and you reject it and you
handle it like that.
And that's one of those things where I
have implementations of my wire trait
that just use Tokyo channels.
So if I'm doing a unit test or I'm doing
simulation or something like that, I
don't actually wire
it up to a TCP socket.
I just have an MPSC channel where frames
go in, frames come out and
it's very usable for testing.
So I think probably my point is that
Sanzio is sort of throwing the baby out
with the bathwater in that, yes, like a
raw async read, async write isn't a great
interface for a protocol and two,
throwing out all of async just so you can
get your state machines to be explicit
state machines is also sort of losing out
on some real benefit of being able to do
like real control flow like match
statements or early returns or labeled
loop breaks and things like that where it
would be egregious to try and keep that.
You'd have to draw the state diagram and
then all of your match statements go,
okay, this is state transition 1.4a where
I go from here to here versus in code,
you just go, okay, I just break from the
loop and it's tick
inner, break tick inner.
You know what I mean?
Yeah, I love this episode because I get
to be the bad person,
the devil's advocate.
So my first argument was like the Tokyo
async read interface is
bad and it should feel bad.
And you're like, well, I did the pass
around ownership of the
buffer, so I don't have that.
And I agree.
So yeah, in that sense, I would agree,
just make everything async and pass
around ownership of the buffer.
Don't borrow anything mutably across
async wait, wait points essentially.
Next on the my agenda as the devil is to
tell you that unfortunately, I don't know
if you've read the documents of the
bright and shiny async rust future that
they wrote a couple years ago.
There's an async work group.
I forget who's on it.
I absolutely have, but not
in two years or something.
I just remember Barbara.
What does Barbara do in this scenario?
There's always a,
it's a user story thingy.
Barbara is one of the voice of the
customer, user story person.
So they always have a name, so they're
supposed to remember.
I forgot all of it, but it's good.
That's my point.
It's been a few years and I think not a
lot of those have come true.
There's been a lot of compiler level
features shipped, which is amazing.
But I'm waiting on
async stack traces, right?
With synchronous code, you can set a
break point somewhere.
You can just look at a, take a dump of
all the stack traces, you
know where it's blocked.
That's hard to argue with.
As long as you cannot do that with async
rust code, you're gonna feel the pain
whenever you're
inspecting a running system.
And Go has that figured out.
Again, I'm infamous Go hater almost, but
I recognize that every rant about Go, I'm
like, they have great runtime
introspection things because Go routines
are the core of everything.
They have their own
runtime and everything.
That's the point.
So debugging is definitely one of them.
And just introspection in general, not
even talking about backtraces, but one of
my motivations when I was working, I'm
not currently working on my HTTP stack
because nobody's
currently funding me for it.
And I have currently other things to do.
But when I was, one of the big
motivations was that, hyper is great and
fine and I love Sean,
but it's always in code.
So you don't know, you don't even know
where resources are spent.
You don't know where the memory goes.
You don't know where it's spending time.
You don't know anything.
It's all very opaque.
You can't really observe the system.
If you compare it with some C or C++, God
forbid, HTTP servers, they
have knobs for everything.
They export all sorts of metrics.
It matters because it runs on every edge
node of your freaking network.
So you need to know where the memory is
spent, what knobs you
can turn to optimize it.
And with Async Rust, I agree with you.
You can just write the code the way you
would with synchronous code.
You have to add a whole other layer on
top of the existing Async Rust thing
because it's designed to be zero cost.
It's also kind of zero benefits.
(Laughs) There's no extra stuff.
There's no system that
lets you know what's going on.
It's just code that's called.
It registers its interest in something
and then it returns all
the way to the runtime.
And then if a task is blocked, you don't
really know where it's blocked.
And that's where you
tell me about Tokyo console.
Is it not, James?
It's a good tool.
And to be honest, in postcard and
poststation, I do use, I'm not sure so
much in postcard, but definitely in
poststation, I use Tokyo pretty heavily.
And one of the cool things that I do is I
actually wrote a Tokyo sync that
serializes the data and puts it in a
database in a rolling ring buffer.
So I can actually even get Tokyo
instrument, not stack traces, but I don't
have good tooling for it.
I've only had to dip into it once.
Right now I just collect it because if
you collect all the data, you can go back
and make a ring buffer and
it doesn't get sent to me.
I don't do the Grafana or Loki thing, but
it's just stored in the local database
same as incoming messages and stuff.
And I have used that in a couple of times
to figure out like, okay, where was this
task and where was it
spinning and things like that.
And I think you're right.
Like you do have to
proactively set that up.
I don't know, it's interesting.
And maybe this just because
I haven't done this in anger.
I haven't had to debug as
many cursed things on that.
I think part of it is that I'm coming
from sort of the
embedded async world first.
So I'm sort of used to everything needs
to be as simple and as dumb as it can be,
just because you don't have much space.
And I try and do a very similar thing.
Like a lot of the existing frameworks and
things like that, I have strong opinions
of, of just like, I'm the kind of person
that really needs to
understand back to front.
And so maybe this is one of those things
where it doesn't, my
opinions don't generalize.
So maybe this is just a learning about
myself, but I think one of the things
that I've done with postcard RPC is
there's only like, there's you the
client, there's the, I do have IO
workers, because you can share one
connection with multiple
clients and things like that.
But the client really just goes, take it
out of this side, encode it, put it in
the wire and on the other side, it pulls
it out of the wire,
goes, does anyone want this?
No, throw it away or yes, you want this,
put it, put it to you.
So like, maybe that's just one of those
things that it works for postcard RPC,
because it is a stupidly simple protocol
compared to HTTP or a TLS
negotiation or things like that.
But yeah, I don't know
what to think about that.
Because you're right, like you do need
those tools and you need to have the
foresight of knowing how to avoid
stepping in the cracks
before you step in the cracks.
Because otherwise, if you design around
cracks, then you'll find
yourself falling in cracks all day.
Let me ask you this, have you ever had to
box a future because
you ran out of stack?
No.
So yes, you don't know
the pain of Async Rust,
clearly.
Because that's a thing, that's the thing
that happens in Rust,
you write Async Rust code.
First of all, recursion doesn't work.
That sucks.
I just, again, explicit
episodes, I can say it.
It sucks, you try to call the function
recursively and it goes, that would be an
infinite state machine.
And it's not wrong.
But you can do that with
synchronous code somehow.
It just keeps recursing
until there's no more space.
So-- I think that's one of those, but
again, I think this is one
of those embedded biases.
Like I have-- You don't
recurse, you never do, you just iterate.
No, I do recurse.
I do love recursion, don't get me wrong.
I'm actually a big fan of recursion.
I don't tend to use it in Async stuff
because it's more IO based.
I tend to use it for more parsing stuff.
But that's usually synchronous code.
I've never needed to
asynchronously recur.
Or I think like once.
I think I found myself needing that about
once and I did run into it.
But that's the whole point.
That's what you just sold everyone on
about is that you can just write it as if
it's regular code and it
magically makes state machines.
But then that abstraction is leaky and it
breaks down when you try to recurse.
It goes, that's an infinitely
sized type, you can't do that.
And it breaks down when you just write
completely regular code and it just so
happens that it made a fricking future
that is four megabyte large and you
didn't have four megabyte left on the
stack and now everything blows up.
And because it's a stack overflow, it's
not really something you can, I don't
even know if you can catch that.
It just brings the whole application down
and then good luck looking at it because
when this started happening, when the
whole Async ecosystem in Tokyo became
viable enough that people started writing
bigger and bigger web
applications with it.
The tooling to find out what's the size
of my future was nil.
He just had to go around and had a bunch
of, I remember I worked at a
company that had those problems.
I had a bunch of friend statements that
printed the size of different values that
were returned from
functions, so features.
And then you just manually decided which
ones you would box so that
the stack wouldn't blow up.
And those are two huge actual problems
that people run into with Async code.
And I'm not even talking about the build
times and everything.
On embedded, we run into these problems a
lot because when you pass things by
ownership up and down the stack, they'll
be double, triple, quadruple the size.
So I mean, if you are setting up, if
you're configuring your serial port and
you wanna give it a 20 kilobyte buffer to
use, if you're creating that buffer and
then passing it by ownership, it might
show up two, three times on the stack
because we don't have
guaranteed RVO and things like that.
And so-
RVO is return value optimization.
And that's the thing where instead of
allocating space on the stack so that a
function can return a value.
Allocate space in the caller, not the
callee, because normally what happens,
yeah, you allocate the destination and
then the function you call also allocates
that on the stack and then it returns it
by copying it from the called function
into the calling function.
So you have two copies of it.
I have a simpler way to
explain the problem, I think.
Imagine you're making a
very big vec from a literal.
How does it work?
I think if you call the vec macro, it
first puts everything on the stack and
then copies it to the
heap and that's an issue.
Yeah.
Yeah, that's basically the idea.
You're trying to put things on the heap
because you don't have enough stack and
you know that you don't have enough
stack, but then actually it's first put
on the stack and then copied to the heap
by the compiler because it's not smart
enough to figure out that it should go
directly on the heap.
And that's one of the forms
of return value optimization.
I guess really what I'm saying is I've
already been traumatized into always
being cognizant of where my
data is and where it's moving.
Yeah, but it's just like
me saying Rust is easy.
It is now.
That's fair.
And I mean, that's a fair, like maybe
this is also sort of me figuring out
before I read a blog post by saying it's
easy, trying to figure out what my biases
are of like, I've already gone through
the trauma and learning of like when
you're thinking about how data flows,
part of that is thinking
about where that data lives.
And if you are doing something where the
data is moving around a lot, you go, I
should probably just put that on the heap
because it's gonna be cheaper to pass
around like a string or a vec than it is
a stack allocated buffer.
So I've never had a
problem and don't get me wrong.
I haven't written nearly as many
production desktop services as a lot of
folks who are listeners have.
I've written a lot more embedded systems
where you do run into similar problems,
but you have the similar situations.
You will preallocate buffers and pass
around a reference to that preallocated
buffer rather than passing the buffer
around for both speed and, and if you
need a buffer that's half of your total
memory, you don't have room for two.
You know what I mean?
Yeah, but what they think it's the code
ends up taking space and you have no
idea, you write code, it
compiles on into a state machine.
The size of the state machine is
completely independent of your will.
Like it's so easy to change
the control flow slightly.
And then instead of having, let's say two
variants of an enum occupy the same
memory space cause
there's a discriminant.
There's very little
optimization in future.
Yeah, that is a big problem.
Small changes can result in big changes
in the size of the future.
And it's really hard to predict.
And it's really easy to get in those
situations where the stack is blowing up
and you have to go on a hunting trip to
figure out where it comes from.
And that happens not just to beginners.
It's like, oh, don't do that.
Just be careful about this.
Which by the way, it's
kind of antithetic to Rust.
Rust is all about the compiler is not
gonna let you get in trouble.
So you don't have to worry.
It's not like you have to follow this set
of rules or else your
dog's gonna get eaten.
That's C, C++, Go, whatever.
Rust is about guiding you and everything.
Okay, this brings me to software is nice
in theory, but in practice it's being
funded by capitalism.
This is the capitalism
moment of the podcast.
Everybody take a shot.
Here's what happened.
Async functionality
is built on generators.
And generators let you write functions
that can yield values.
You are familiar with them if you write
Ruby code, for example, you write a
function and then you can return, but you
can also just yield a value.
So you can do iterators with that.
That's one way of doing iterators.
You write a function that can yield one,
two, three, four, or five and you're
iterating over all the numbers.
Rust doesn't do that.
Rust has you have a type that implements
a trade that you can call a function on
and then it returns with a value.
And you keep calling a
function to return more values.
It doesn't have this yield thing, but
there are generators in Rust.
They're just permanent and stable.
They haven't been stabilized at all.
Whereas async, which is built on top of
generators has been stabilized.
Why is that?
Because there are companies with a lot of
money who really want to be able to write
async code and not enough company with
not enough money who want
to be able to use generators.
I want to be able to use generators, but
I can't bankroll half of the Rust
foundation by myself.
I wish I could.
So really you're not
asking for async James.
You're asking for generators.
And that's what I want too.
Cause you don't really, the async part is
kind of not necessarily relevant because
another argument I was going to throw at
you and I'm going to let you respond.
I'm going to let you finish, but
sometimes you don't
want to pull in async.
Sometimes all you want
is a nice small CLI tool.
And I've defended Rust async forever.
So I'm really enjoying playing the other
part today, but sometimes you just want
something that grabs some webpage and I
don't know parses the CSS or whatever.
You don't want to bring
in Tokyo and everything.
You just want some
bytes from the network.
It's fine for you to
do blocking syscalls.
You're not doing a
high performance server.
People have a point that is true.
And so if you write everything as async
code, you can technically have actually
synchronous code that implements async
and if you can pretend that it's async
but actually it blocks and
it's going to mostly work.
But as soon as you call something from
Tokyo that is actually asynchronous, the
compiler will not warn you about that.
It will just fail at runtime and it's
still going to bring in
a bunch of dependencies.
There's still no good answer to writing
code that will work both in an
asynchronous context
and a synchronous context.
And the sense IO approach is kind of the
only way to do it right now.
So you make a lot of really good points.
The, the, the, the, the.
We can take it one at a time.
I know I said a lot.
I'm sorry.
No, no, no, it's okay.
It's where do you
start peeling the onion?
Am I the onion in the metaphor?
The onion is ours.
It's emotional right now.
We have an onion to go.
Definitely like I see,
I understand the desire.
It's nice to write blocking code.
It's especially like in terms of learning
curve, there are deficiencies in a lot of
things and it does require some extra
learning and it's, it's sort of like the
transition from like not Rust to Rust.
You kind of get that transition again
with Rust to async Rust.
And I think it's oversold a lot.
Like I'm kind of stepping carefully here
because there's a lot of, you know,
people go on the internet and they copy
and paste old arguments even when those
old arguments aren't
necessarily true anymore.
Async has made a lot of improvements and
I think the learning materials and the
tooling and the compiler have come a long
way and there's still, you know,
improvement to be had where we're at the
standard of where
we'd probably like to be.
But also there's a lot of people copying
and pasting arguments from five years ago
and it's very tiring to people who are
working on those improvements or
libraries and things like that, where
people are just, you're like, you don't
know, you're just memeing right now.
And that's frustrating.
So part of it, that's part of why I'm
dancing around this.
The other one is it is just a different
beast and I will agree with it that it is
a different beast, but it allows for some
very cool things like boats has good, a
thousand good ways of describing this,
but just being able to throw things like,
hey, I want to slap a timeout on this and
call dot timeout and seed controller, or
I want to be able to concurrently run
these and race them against each other
and those kinds of things where it's the
intratask concurrency, doing it for high
performances is a reason, but it's not
the only reason why you would use async.
And there will be in the show notes a
link to boats is post about intratask
concurrency and why futures matter.
Yeah.
Because Amanda is the best.
And there are things where like Tokyo
doesn't have to be this huge dependency.
Like it can, you can turn off a lot of
features and you can do those.
And yes, it is a little setup to do, but
I have my favorite setup of Tokyo in my
command buffer where I do, I hit control
R cargo add Tokyo and it adds the
features that I want to the project that
I want and it's pretty quick to set up.
But I mean, okay.
So first off, I just want to say, I don't
even have to argue with you, even though
that's kind of what we're doing for fun.
Just remember that we're
friendly towards each other.
We're not actually fighting.
Okay.
Don't do this at home, but
we're not actually fighting.
We both probably mostly agree on all of
these points, but it's
interesting to dance around.
But I think I don't have to argue with
you because I can just wait two years and
one of two things will happen.
One, you will feel the pain of your
growing application with async code and
figure out that it's not just beginners.
It's not just four years ago.
It is like current Ross
that has very real problems.
And then you will see
that I'm actually right.
Or the compiler will get so much better
that it will make me wrong.
And in either case, I will be happy
because I'll be either right
or the world will be better.
So I went either way.
I think the thing that is frustrating me
the most right now is that most of my
responses to this are that it requires
attention to how you design your systems.
Like one of the things that I said at the
last talk I gave was, sometimes the
problem with a good tool is that you use
it too far beyond what it should be used
because it's so pleasant to use.
And I think async can do that where you
do still have to think, I don't totally
buy the thing of async is
just like blocking code.
Like it's straight line,
you don't have to think about.
I do think from a systems perspective,
you have to think about what your code is
doing, how you architect it, where the
data is stored, where the data is going,
where you draw the
lines of things like that.
And I think that's like a
systems design question.
And I don't think you are well-served by
pretending that blocking code and async
code are the same,
because I don't think they are.
They're very similar and it's nice that
you can do that, but I think that's sort
of a misfeature of that.
But it is one of those things that I've
been doing, you know, async on desktop
for maybe two or three years for
relatively different sized applications
and async on embedded for maybe four
years or something like that.
And I really did have a
learning curve of that.
But I think that's the same kind of thing
in desktop Rust is like you're saying
with recursion, you can blow your stack
and recursion very easily,
and you'll have to learn that.
Or you have to learn that you need to
lock stdio before you print, or otherwise
all of your benchmarks are very silly.
Or there are definitely like systemic
things that you need to learn.
And I do think that they're higher for
async, but I think it's-- I don't know.
I don't like that my answers get good,
because that's not a good answer and it's
not really a scalable answer.
And I disagree.
I disagree with you, James, and that's
beautiful, because
that makes a good podcast.
Because I don't think it's very honest,
because I think if we're adding async
functions and we're telling everyone
they're exactly like synchronous
functions, except you
sometimes have to know how to run.
But the goal, with each addition of
features, was to be OK, so that thing
that used to not be possible with async
code is now possible with async code.
There's a clear trend to making you be
able to do everything you can do with
synchronous code also do
with asynchronous code.
So clearly the aim is for you
to not have to worry about it.
The fact that you can't
do recursion is a problem.
There's a bunch of new things that were
added recently that lets
you do lending iterators.
What did we have recently?
We had Gatsk.
Async closures.
Those things made sense.
Everybody thought that
it wasn't a big invention.
Everybody wanted them.
They were just hard to implement in the
compiler, because it's not
a trivial transform at all.
And there's this
borrow checking involved.
It's really complicated when you get into
the details of how it's implemented.
But everything made sense.
From the moment you were
like, OK, we can make this async.
We can make an await point actually
return and then come back to the exact
same state in the function.
The whole universe of possibilities of
what we could do with
async Rust was already drawn.
And the fact that some of
it is currently problematic.
You can't recurse.
Sometimes the features get
too big and the stack blows up.
You have to debug that.
It's a compiler bug.
It's not-- people are being unreasonable
and asking too much of async Rust.
It's like, this should be fixed.
But because we live in the real world
with limited resources and limited
funding, it's good enough that the
biggest users of async Rust are kind of
OK with where it's at.
And things are making slow progress.
But there's no big rush
because you can already do so much.
Like you say, if you just like sometimes
you run into corner cases and you just
kind of work around them.
But as far as I'm
concerned, it's a compiler bug.
It's just like the last x% are the
hardest because some of them involve
fundamental changes to the compiler.
Yeah.
I don't entirely disagree.
And I do think a lot of
those should be better.
I think-- I don't know.
Some of them are given more-- or they
seem like more of a
problem than they are.
Well, I don't know.
I think you just got lucky and you just
didn't run into too many of those yet.
I think we should check
in with you in six months.
I mean, I'm happy to.
But I mean, Postation has been
exceedingly pleasant to develop.
And even though my database is a blocking
database-- so I'm using Fyall as my
database and Fyall is blocking.
But I mean, there are ways to develop
that where it becomes not
particularly challenging.
Or it is blocking, but it's a
transactional database.
And my queries aren't large.
And I'm not recursing over those.
Most of my blocking queries are
insertions only and things like that.
I think you're in the honeymoon period.
And you're trying to-- I just have to
disagree with people who run into
problems just don't know enough or they
had older Rust versions.
I just disagree with that.
I guess the point that I'm really trying
to make is that Sanzio forces you to
disregard all of the benefits of async
for the cost of compatibility.
And it's one of those challenging things.
Because when it comes to making sure that
your state machines are clear and correct
and things like that, it's way easier to
lose things in the diff of a very large
set of match statements
rather than linear code.
But generators.
It's not the answer you expected.
But I want you to take a minute.
No, I'm very familiar with generators.
Think about it.
And I do think there are some cases where
they absolutely would be the thing that
you want, like being
able to have possible--
Because your main argument is it sucks
the right state machines in
Rust, which is absolutely true.
I've written a bunch.
I know that.
You just do macros.
It is, but I prefer protocols.
So I'm very heavy in
protocol stuff right now.
You do really want to say, I want to send
something and await the result.
And I want to timeout, or I
want to receive the result.
And if-- or I want to basically mux on a
timeout occurs, a wrong message comes in,
a correct message comes in, or a giant
cancellation flag, or some external
signal has been
received and things like that.
And you really do want to be able to say,
OK, I'm going to receive.await, and then
I'm going to do a select
statement on three items.
And I think I'd agree with you in that
you could cover a fairly large subset, or
Sanzio could be made
much better with generators.
I would definitely agree with you on
that, in that you could
boil your state down to that.
But I guess what I'm saying is you can
get a lot of the benefit by having good
abstractions for things like that, and
that sometimes existing abstractions are
not great, and they force you into
corners that end up
making things more challenging.
Whether Async can improve or not, I
think, I'll agree with you.
I absolutely can.
But also that I think there is some level
of people are more scared
than the actual level there.
But again, maybe I just
haven't seen the right horrors yet.
The thing that I'm really getting at is
that I've seen a glimpse of something
that can be really pleasant in Async, and
writing protocol design, particularly
designing my own protocol from scratch
with Async Rust in mind to begin with,
and designing data structures around
that, and designing interactions with the
understanding of, hey, I know I'm going
to need a timeout or error or success and
being able to really natively encode that
as Async logic has been
tremendously pleasant.
And just thinking about how I would do
all that, if I had to turn everything
inside out to do Sans I.O., is one of
those really challenging things to me,
because I don't think it
would be nearly as pleasant.
And maybe this is the benefit of
designing something simple from scratch
with Async in mind to begin with, rather
than something that was encoded 20 years
ago from an RFC that has so many edge
cases and performance
impacts and things like that.
I think there's some hesitation to be
made instead of always going with Sans
I.O., because I think there is some
really interesting and clever and
pleasant, and really things that make
your code easier to read and more robust
if you address them as Async challenges
to begin with, instead of always sort of
landing on Sans I.O., even though it is a
very good tool in very specific cases.
Now that I'm done playing Devil's
Advocate, I will agree with you that,
one, it's not that bad
to write Async Rust code.
I've been writing
that for three years now.
So another very interesting read you can
have that is going to be in the show
notes is an article from the maintainer
of Cargo Next Test, Rain, who explains
why Cargo Next Test, how Cargo Next Test
uses Async and how it would be so hard to
do that using synchronous syscalls, it
would not be possible.
So the Next Test uses Tokyo very, very
efficiently, not
efficiently, it's great effect.
Effectively.
Finally, I wanna say that my, if you give
up the idea of having nice, light,
synchronous binaries, and you're all in
Async land, you've just kind of accepted
you're gonna have some sort of runtime,
then I think I would be fine if we all
standardized on an IOU ring compatible
abstraction because if you have that, you
can implement, you can implement that
interface efficiently on top of something
like Tokyo, but you
cannot do the reverse.
So one of them is strictly
more flexible than the other.
So I'm just really sad that Tokyo, like
the current Async retrades in the Rust
ecosystem are the de facto standard
because you cannot do
your ring with them.
So if we could standardize on that, then
we'd notice, or if we have a frame based
interface, like you're doing with
PostgreSQL, then that's fine too, because
it's also flexible enough.
I think people should be less afraid to
define their own interfaces.
I think there's something very powerful
of going, look, portability is fine, and
if people want to polyfill portability
onto this, but there's something to be
said about defining exactly the interface
you actually want and to use that.
Because I think that's one of the things
that I've done in my projects, and I
found there's a time for portability and
there's a time for being specific.
And I think getting that right, I think
goes a really long way.
Because like I said, if I can say frames
or frames, it doesn't matter whether it's
IOU ring or blocking IO, well, you
probably couldn't use blocking IO, but it
doesn't really matter how it gets there.
I say, that's your problem.
I'm saying what I need, and you're
telling me how you want to do it, which
gives a lot of customization back to the
integrator, which I think it helps a lot
of these challenges.
My mind immediately goes to Tokyo Rust
LS, which is a very leaky abstraction,
because TLS is not just the TCP sockets.
There's a side, what is it called?
There's events, there's
alerts that can happen.
You can know when the
keys are being rotated.
You can know where there's some sort of
exception or whatever.
And that's not part of
the async rate interface.
So on Linux, you have a file descriptor,
and then you have
some way to get to that.
I think it's called ancillary data.
I don't know exactly.
But essentially, it's not
just bytes go in, bytes come out.
There's control data, there's things on
the side, and that's completely lost in
the Tokyo Rust LS abstraction and other
Rust LS abstractions.
But it's not in the Rust LS crate, which
does implement the protocol
in some form of sense IOUA.
So yeah, I would agree with you there.
Well, then here's to
the honeymoon period.
(Laughing)
And we'll check back on
you in six to 12 months.
Yeah, we'll do the follow-up.
I think that'll be good.
Season three.
We will.
Beautiful, that was a cool ending.
(Laughing)
Also long fucking honeymoon, man.
Cheers.
(Upbeat Music)
This episode is sponsored by Depot, the
build acceleration platform that's on a
mission to make all builds near instant.
If you're tired of watching your builds
and GitHub actions crawl like the modern
day equivalent of paint drying, give
Depot's GitHub actions runners a try.
They're up to 10X faster with unlimited
concurrency, faster caching, support for
Linux, macOS, and Windows, and they plug
right into other Depot optimizations like
accelerated container image builds and
remote caching for Bazel,
TurboRepo, Gradle, and more.
Depot was built by developers who were
tired of wasting time waiting on builds
instead of shipping.
It's made for teams that wanna move
faster and stay focused
on what actually matters.
That's why companies like Posthog use
Depot to cut build times from over three
hours to just three minutes, saving tens
of thousands of build hours every week.
Start your free seven-day trial at
depot.dev and let them know we sent you.
(Upbeat Music)