First, please keep in mind that this proposal was written before we had ´sending´ and isolated parameters (I think), so it doesn't talk about closures that use these features. Basically, as I understand this section, it explains the difference of "regular old closures" and the (back then) new ´@Sendable´ closures.
It explains how isolation is inferred for them. Normally, any "regular old closure" is (at compile time) isolated to the same context that it is defined in.
So if you're in a function defined in an actor, the closure is also defined in an actor.
However, Task.detached´ takes a ´@Sendable annotated closure, so the closure defined in the (actor isolated) ´endOfMonth´ function in the example of that section is not a "regular old closure". The proposal then explains why in this case, the isolation is not kept. Simply put, it doesn't make sense, as @Sendableby definition means it will be potentially called in another isolation domain. This means it cannot be isolated to the actor and the only logical alternative is to implicitly assume it's non-isolated (i.e. not isolated to any context).
The example below the section then gives an example for a "regular old closure", one that is defined in the function and then passed to ´forEach´.
For what it's worth, this of course also applies to functions defined on non-actor types that are individually isolated to an actor (e.g. to ´@MainActor´).
Thanks for your clear answer. For non-Sendable closures (i.e. regular old closures), what does “isolated to the same context” mean? The way I understand isolation (actor isolation in particular) is that it is unsafe to reference a property declared inside the actor from another actor unless I use an asynchronous method to do so. On the other hand, it is safe to read or mutate a property within the same actor. If this is correct, therefore, does non-isolated mean that these rules don’t apply in a way?
If I understand you correctly, those are basically two questions, so first:
What do I mean with "isolated to the same context"? What's "context" here?
"Context" in a way means "the surrounding code" or "the function/scope the closure is defined in". A function you define on an actor is, naturally, in the isolation domain that is the actor. So if you then, in the implementation of that function, define a "regular old closure", it has the same isolation, i.e. also the actor.
It may help a little to think of as "non-isolated" to be "yet another isolation domain". it's the isolation domain of "not being isolated to anything specific" or "free to be used from any other isolation domain". So if I say that a closure is "isolated to the same context [as something else]", I just mean "whatever isolation domain this context belongs to, the closure has the same thing".
Also note that this does indeed happen at compile time, so wherever you write the actual source for a closure, just check which isolation the code before/around it has and you know what the closure is isolated to (which may, again, also be non-isolated).
" does non-isolated mean that these rules don’t apply in a way?"
Well, when we're speaking of closure definition in actor definitions, yes. The reason why any @Sendable closure you define in an actor (like in the proposal's example with Task.detached) is indeed interpreted differently (i.e. it does not have the same isolation as its surrounding context, i.e. the actor) is simply that it doesn't make sense to treat it the same as a non-@Sendable closure. The only reason why functions like Task.detached require a @Sendable closure in the first place is that they want to run the closure in a different isolation that the context where the closure is defined. As the example shows, this means that when you're inside the closure and want to access actor state, you have to await it.
Basically the entire section of the proposal explains that "Hey, look, this is how you can isolate state and run code in a concurrent world. But careful, when you somehow explicitly leave your own isolated island, you're obviously no longer on your own island."[1]
Before we had structured concurrency in the language syntax, everything basically always had the same isolation as the surrounding code: non whatsoever (syntax wise). You could always (synchronously) refer to stuff in a closure that was actually defined outside of it. However, when the closure ran in a different thread (let's say you pass the closure to DispatchQueue.main.async, this was actually an invisible problem:
class HurtMe {
var someValue: String?
func pain() {
DispatchQueue.main.async {
// imagine something complicated here, etc.
self.someValue = "..."
}
}
The compiler doesn't somehow "judge" the closure I define and send to async. There was no way to express this and it doesn't care that I could also modify someValue while the queue is running the closure. With structured concurrency, everything is somehow marked during compile time about "where" it will run. Either implicitly or explicitly.
And in the case of @Sendable closures defined in an actor-isolated context (i.e. any "code block" or scope) the new syntax's meaning is that it is associated to the non-isolated "where", unlike non-@Sendable closures[2]
my apologies to the original authors for summarizing their great work in this jokingly unhelpful manner ↩︎
btw, by now there's even more to consider here, as isolated parameters or sending offer additional semantics, but that's beside too much for this post now, I think. ↩︎