that closure will inherits the isolation of the surrounding context by default. Often this is desirable, but if it isn't, there's no way to turn it off2.
Great to acknowlage that! Yes detach is too big of a gun to solve this.
Third, there is new special default argument expression,
#isolation
, which expands to the static actor isolation of the caller. This can be used for any parameter, but when the parameter is specifically declaredisolated
, this has the effect of implicitly propagating the static isolation of the caller to the callee.extension Collection { func sequentialMap<R>(isolated isolation: (any Actor)? = #isolation, transform: (Element) async -> R) async -> [R] {
Distributed complicates this a bit... the simplest solution for today might be to allow
extension Collection { func sequentialMap<R>(isolated isolation: (any DistributedActor)? = #isolation, transform: (Element) async -> R) async -> [R] {
since AnyActor
is a marker protocol we can't use it to express isolation... unless we'd like to re-consider that for Swift 6 and then make AnyActor a real protocol in which case we could:
extension Collection { func sequentialMap<R>(isolated isolation: (any AnyActor)? = #isolation, transform: (Element) async -> R) async -> [R] {
I forget why we didn't make it a real protocol to begin with though... I'm sure there was a good reason that perhaps we have to stick to still.
The existing type-checking rule for
isolated
parameters can be summarized as "the type must be convertible toany Actor
; the new rule can be summarized as "the type must be convertible to(any Actor)?
.
If spelling out a formal rule we should acknowlage DistributedActor I guess here. Or consider the AnyActor change mentioned above.
So it'd become:
... the new rule can be summarized as "the type must be convertible to
(any Actor)? or (any DistributedActor)?
".
Unless we change AnyActor from:
@_marker
@available(SwiftStdlib 5.1, *)
public protocol AnyActor: AnyObject, Sendable {}
into:
@available(SwiftStdlib 5.1, *)
public protocol AnyActor: AnyObject, Sendable {
// yes, both Actor and DistributedActor have the same requirement like this, so we "lift" this requirement
nonisolated var unownedExecutor: UnownedSerialExecutor { get }
}
- references to
let
bindings formed from the value (e.g.if let a = optA
orguard let a = optA
). For example, if a function is isolated tooptA: A?
, then the method calloptA?.run()
is known to not cross an isolation boundary.
This seems to imply that this would work? But I'm not sure I'm not reading too much into this sentence?
actor D {
func work() {}
}
func daDoRunRunRunDaDooRunRun(worker: isolated Worker?) async {
guard let worker else { return }
// assume isolated!
worker.work()
worker.work()
}
We propose that global actors be semantically limited to be singleton.
Absolutely agree on that! In original proposals I thought we implied that it should be the same actor underlying but I guess we have not strictly required so...?
This permits Swift to assume that functions isolated to a non-optional value of a global actor type are actually isolated to the global actor exactly as if they were annotated with the attribute.
Yes, that's good and what I'm after for isolating Task.init's closure to a global actor right away...
We'd combine this with the inherit feature to be able to express–what we're unable to express today–in the form of:
@MainActor func hi() {}
Task(on: MainActor.shared) {
hi() // no await
}
// where the init is:
init<Act: Actor>(
on actor: isolated Act,
// ...
operation: __owned @inheritActorContext @Sendable @escaping () async -> Success)
Note that the operation
closure is @Sendable
.
The grammar of a closure expression's capture list is modified to allow the
isolated
keyword:capture-list-item → capture-specifiers identifier capture-list-item → capture-specifiers identifier = expression capture-list-item → capture-specifiers self-expression capture-specifiers → 'isolated'? capture-strength-specifier?
Missing 'nonisolated'?
here?
This is awesome and the "what actor is calling me" I've longed for for tracing surprisingly!
This really helps us in distributed-tracing in two ways:
- so the withSpan's closure can be isolated to the calling actor
- we'll finally get the ability to log which actor is making a call, this is huge (!)
func withSpan(...,
isolation: (any Actor)? = #isolation,
_ operation: (any Span) async throws -> T) {
if let actor = isolation,
let identifiable as? Identifiable {
span["actor.id"] = "\(identifiable.id)"
}
}
and since the isolation paramter isn't isolated
If the corresponding argument to a parameter with
@inheritsActorIsolation
in a direct use of the declaration is a closure expression that does not include an isolation specification, then the static isolation of the closure is the same as the static isolation of the calling context unless the calling context is isolated to a value of a non-global-actor type that is either not captured or capturedweak
by the closure (in which case the closure is not isolated). (This rule is the same as is used by theTask
initializer.)
I think this is all good, could use an example perhaps to add to the text:
actor Mailer {
func mail() {
Task {} // existing init; no parameters -> infer from static isolation of calling context
}
}
and the new semantic:
Task(on: Mailer()) { ... isolated to that mailer ... }
where we'd declare it as:
init(
on actor: isolated some Actor,
// ...
operation _operation: __owned @inheritActorContext @Sendable @escaping () async -> Success)
right?
We could combine it with the #isolation actually I guess, to end up with:
init(
on actor: isolated (some Actor)? = #isolation,
// ...
operation _operation: __owned @inheritActorContext @Sendable @escaping () async -> Success)
which gives exactly the existing Task{}
semantics but allows overriding it by passing an actor...
That looks very good!
Task { [weak self] in
// not isolated... we don't know if self is .some() or .none
guard let self = self else { return }
// assume `isolated self` from here onwards
}
This is the inverse of what happens in initializers where the isolation deteriorates -- Kavon implemented that logic in initializer safety work.