Discussion: "Notes on structured concurrency, or: Go statement considered harmful"

I forgot to reply to this at the time, but it just came up again in chat, and I ended up writing a little reply that I think might be a good complement to Cory’s post that @belm0 linked to. So I’ll paste it here too:

yeah, python async/await is definitely a two-color system
I think my main frustration with that post is that it takes for granted that having two function colors is obviously a bad thing
…though to be fair, I can see how if you’re starting with js callbacks as your main experience with them, then it does feel pretty obviously problematic
but fundamentally, function color is a way to encode certain behavioral properties into your type system, like “this function does IO”. The thing about that property is that it’s transitive over call stacks: if A calls B and B does IO, then A does IO too. So if you think “this function does IO” is a useful thing to encode in your type system, then function color is how you do it. And that plausibly is a useful thing to include in a type system, e.g. haskell does it.
so IMO the question is really whether any given function color system has a good enough ergonomics/functionality tradeoff to be worthwhile.
in trio, the property it encodes is “could release the GIL and could be interrupted by cancellation”, which are both pretty important behavioral properties! putting them in the type system has a lot of value. and migrating to async/await is painful, but it’s way less painful than callbacks.

Yeah, I was being a bit hand-wavy there. The point is that you can now see which code is running inside the with block and which code isn’t: the nursery.start_soon call happens inside the with block, and indeed, the file will remain open for the start_soon call. But if you know what start_soon does (it schedules a task to start running in the near future, but returns before it’s actually started), then hopefully it should be pretty obvious that closing the file after calling start_soon is not what you want. And – crucially! – you can tell that just from looking at the source code inside the with block; you don’t have to go look inside read_file.

Here’s another example that might make this clearer, that doesn’t involve concurrency at all. It’s totally possible to use regular with blocks in a broken way too, for example:

with open("my-file") as file_handle:
    another_var = file_handle
read_file(another_var)

Obviously this code won’t work correctly, but you can write it if you want. So in general, with blocks don’t like, strictly guarantee that you can never access a closed file handle. (For that you need something like Rust’s lifetime tracking.) But in practice, it’s OK; people don’t write code like this by accident, because it’s obviously wrong. And once you’re used to nurseries, then your “bad” example is obviously wrong too, in basically the same way.