Skip to content

Commit f94017b

Browse files
committed
simplify the sendable / initializer relationship
I decided later in the review process that the initially-proposed rules were too complex for what was gained. This commit changes the rules to match my change-of-heart as posted here: https://forums.swift.org/t/se-0327-second-review-on-actors-and-initialization/54093/8
1 parent fdf5f30 commit f94017b

File tree

1 file changed

+60
-63
lines changed

1 file changed

+60
-63
lines changed

proposals/0327-actor-initializers.md

Lines changed: 60 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
- [Syntactic Form](#syntactic-form)
3232
- [Isolation](#isolation)
3333
- [Sendability](#sendability)
34-
- [Delegation and Sendable](#delegation-and-sendable)
3534
- [Deinitializers](#deinitializers)
3635
- [Global-actor isolation and instance members](#global-actor-isolation-and-instance-members)
3736
- [Removing Redundant Isolation](#removing-redundant-isolation)
@@ -40,6 +39,7 @@
4039
- [Introducing `nonisolation` after `self` is fully-initialized](#introducing-nonisolation-after-self-is-fully-initialized)
4140
- [Permitting `await` for property access in `nonisolated self` initializers](#permitting-await-for-property-access-in-nonisolated-self-initializers)
4241
- [Async Actor Deinitializers](#async-actor-deinitializers)
42+
- [Requiring Sendable arguments only for delegating initializers](#requiring-sendable-arguments-only-for-delegating-initializers)
4343
- [Effect on ABI stability](#effect-on-abi-stability)
4444
- [Effect on API resilience](#effect-on-api-resilience)
4545
- [Acknowledgments](#acknowledgments)
@@ -598,114 +598,96 @@ But, the delegating initializers of an actor have simpler rules about what can a
598598

599599
### Sendability
600600

601-
The delegating initializers of an `actor`, and all initializers of a GAIT, follow the same rules about Sendable arguments as other functions. Namely, if the function is isolated, then cross-actor calls require that the arguments conform to the `Sendable` protocol.
602-
603-
All non-delegating initializers of an actor, regardless of any flow-sensitive isolation applied to `self`, are considered "isolated" from the `Sendable` point-of-view. That's because these initializers are permitted to access the actor's isolated stored properties during bootstrapping.
604-
605-
These two rules force programmers to correctly deal with `Sendable` values when creating a new actor instance. Fundamentally, programmers will have only two options for initializing a non-`Sendable` stored property of an actor:
601+
When passing values to any of an `actor`'s initializers, from outside of that actor, those values must be `Sendable`.
602+
Thus, during the initialization of a new instance, the actor's "boundary" in terms of Sendability begins at the initial call-site to one of its initializers.
603+
This rule forces programmers to correctly deal with `Sendable` values when creating a new actor instance. Fundamentally, programmers will have only two options for initializing a non-`Sendable` stored property of an actor:
606604

607605
```swift
608606
class NotSendableType { /* ... */ }
609607
struct Piece: Sendable { /* ... */ }
610608

611609
actor Greg {
612-
var ns: NonSendableType
610+
var ns: NotSendableType
613611

614-
// Option 1: a non-delegating init can only take
615-
// Sendable values and use them to construct
616-
// a new non-Sendable value.
612+
// Option 1: an initializer that can be called from anywhere,
613+
// because its arguments are Sendable.
617614
init(fromPieces ps: (Piece, Piece)) {
618-
self.ns = NonSendableType(ps)
615+
self.ns = NotSendableType(ps)
619616
}
620617

621-
// Option 2: a delegating and nonisolated-self init
622-
// can take a non-Sendable value and allow you to
623-
// pass the Sendable pieces to a non-delegating init.
624-
init(with ns: NonSendableType) {
618+
// Option 2: an initializer that can only be delegated to,
619+
// because its arguments are not Sendable.
620+
init(with ns: NotSendableType) {
625621
self.init(fromPieces: ns.getPieces())
626622
}
627623
}
628624
```
629625

630-
As shown in the example above, you _can_ construct an actor that has a non-`Sendable` stored property. But, you must be able to create a new instance of that type from `Sendable` pieces of data. The two options above provide ways to either accept the pieces directly in a non-delegating initializer, or to rely on a delegating initializer to start with non-`Sendable` values. This effectively forces programmers to construct a new, fresh instance of the non-Sendable value within the non-delegating initializer.
631-
632-
#### Delegation and Sendable
633-
634-
It's tempting to think that all delegating initializers can accept non-Sendable values from any caller, but that's not true. Whether it is safe to pass a non-Sendable value to a delegating initializer still depends on the isolation of the caller.
635-
636-
For example, an `async` delegating initializer has an `isolated self`, so it has access to the stored properties after delegating. If we were to allow non-Sendable values into this initializer _without_ paying attention to the isolation of its caller, then an invalid sharing of non-Sendable values can happen:
626+
As shown in the example above, you _can_ construct an actor that has a non-`Sendable` stored property. But, you should create a new instance of that type from `Sendable` pieces of data in order to store it in the actor instance. Once inside an actor's initializer, non-Sendable values can be freely passed when delegating to another initializer, or calling its methods, *etc*. The following example illustrates this rule:
637627

638628
```swift
639629
class NotSendableType { /* ... */ }
640630
struct Piece: Sendable { /* ... */ }
641631

642632
actor Gene {
643-
var ns: NonSendableType?
633+
var ns: NotSendableType
644634

645-
init(with ns: NonSendableType) async {
646-
self.init()
635+
init(_ ns: NotSendableType) {
647636
self.ns = ns
648637
}
649638

639+
init(with ns: NotSendableType) async {
640+
self.init(ns) // ✅ non-Sendable is OK during initializer delegation...
641+
someMethod(ns) // ✅ and when calling a method from an initializer, etc.
642+
}
643+
650644
init(fromPieces ps: (Piece, Piece)) async {
651-
let ns = NonSendableType(ps)
652-
await self.init(with: ns) // ✅ OK
653-
assert(self.ns == ns)
645+
let ns = NotSendableType(ps)
646+
await self.init(with: ns) // ✅ non-Sendable is OK during initializer delegation
654647
}
648+
649+
func someMethod(_: NotSendableType) { /* ... */ }
655650
}
656651

657-
func someFunc(ns: NonSendableType) async {
658-
let ns = NonSendableType()
659-
_ = await Gene(with: ns) // ❌ error: cannot pass non-Sendable value across actors
652+
func someFunc() async {
653+
let ns = NotSendableType()
654+
655+
_ = Gene(ns) // ❌ error: cannot pass non-Sendable value across actor boundary
656+
_ = await Gene(with: ns) // ❌ error: cannot pass non-Sendable value across actor boundary
657+
_ = await Gene(fromPieces: ns.getPieces()) // ✅ OK because (Piece, Piece) is Sendable
660658

661-
_ = await Gene(fromPieces: ns.getPieces())
659+
_ = await SomeGAIT(isolated: ns) // ❌ error: cannot pass non-Sendable value across actor boundary
660+
_ = await SomeGAIT(secondNonIso: ns) // ❌ error: cannot pass non-Sendable value across actor boundary
662661
}
663662
```
664663

665-
In the example above, both `Gene.init(with:)` and `Gene.init(fromPieces:)` are both delegating initializers that are `isolated self`. The difference is that `init(with:)` takes a non-`Sendable` argument, whereas `init(fromPieces:)` only takes `Sendable` arguments. It would not be safe to permit a call from `someFunc` to `init(with:)` because that would mean passing a non-`Sendable` value across actors. But, it's OK to delegate from `init(fromPieces:)` to `init(with:)` because the isolation is matching!
664+
For a global-actor isolated type (GAIT), the same rule applies to its `nonisolated` initializers. Thus, upon entering such an initializer from outside of the actor, the values must be `Sendable`. The differences from an `actor` are that:
666665

667-
<!--
668-
Here's another more exhaustive example to show all the different corner-cases:
666+
1. The caller of the first initializer may already be isolated to the global-actor, so there is no `Sendable` barrier (as usual).
667+
2. When delegating from a `nonisolated` initializer to one that is isolated to the global actor, the value must be `Sendable`.
669668

670-
```swift
671-
class NotSendableType { /* ... */ }
669+
The second difference only manifests when a `nonisolated` and `async` initializer delegates to an isolated initializer of the GAIT:
672670

673-
actor George {
674-
var ns: NonSendableType
671+
```swift
672+
@MainActor
673+
class SomeGAIT {
674+
var ns: NotSendableType
675675

676-
init(anyNonDelegating ns: NonSendableType) {
676+
init(isolated ns: NotSendableType) {
677677
self.ns = ns
678678
}
679679

680-
init(delegatingAsync ns: NonSendableType) async {
681-
self.init(anyNonDelegating: ns) // ✅ OK
682-
self.ns = ns // ✅ OK
683-
}
684-
685-
// ^^^ the above can only be delegated to from another init
686-
// ---
687-
// vvv the below can be called from outside the actor
688-
689-
init(delegatingSync ns: NonSendableType) {
690-
self.init(anyNonDelegating: ns) // ❌ error: cannot pass non-Sendable value across actors
691-
self.ns = ns // ❌ error: cannot mutate isolated property from nonisolated context
680+
nonisolated init(firstNonIso ns: NotSendableType) async {
681+
await self.init(isolated: ns) // ❌ error: cannot pass non-Sendable value across actor boundary
692682
}
693683

694-
nonisolated init(delegatingNonIsoAsync ns: NonSendableType) async {
695-
self.init(anyNonDelegating: ns) // ❌ error: cannot pass non-Sendable value across actors
696-
self.ns = ns // ❌ error: cannot mutate isolated property from nonisolated context
684+
nonisolated init(secondNonIso ns: NotSendableType) async {
685+
await self.init(firstNonIso: ns) //
697686
}
698687
}
699-
700-
func someUnrelatedCaller(ns: NonSendableType) async {
701-
_ = George(anyNonDelegating: ns) // ❌ error: cannot pass non-Sendable value across actors
702-
_ = await George(delegatingAsync: ns) // ❌ error: cannot pass non-Sendable value across actors
703-
_ = George(delegatingSync: ns) // ✅ OK
704-
_ = await George(delegatingNonIsoAsync: ns) // ✅ OK
705-
}
706688
```
707-
-->
708689

690+
The barrier in the example above can be resolved by removing the `nonisolated` attribute, so that the initializer has a matching isolation.
709691

710692
### Deinitializers
711693

@@ -871,6 +853,21 @@ One idea for working around the inability to synchronize from a `deinit` with th
871853

872854
The primary danger here is that it is currently undefined behavior in Swift for a reference to `self` to escape a `deinit` and persist after the `deinit` has completed, which must be possible if the `deinit` were asynchronous. The only other option would be to have `deinit` be blocking, but Swift concurrency is designed to avoid blocking.
873855

856+
### Requiring Sendable arguments only for delegating initializers
857+
858+
Delegating initializers are conceptually a good place to construct a fresh
859+
instance of a non-Sendable value to pass-along to the actor during initialization.
860+
This could only work by saying that only `nonisolated self` delegating initializers
861+
can accept a non-Sendable value from any context. But also, an initializer's
862+
delegation status must now be published in the interface of a type, i.e.,
863+
some annotation like `convenience` is required. Eliminating the need for
864+
`convenience` was chosen over non-Sendable values for a specific kind of delegating
865+
initializer for a few reasons:
866+
867+
1. The rules for Sendable values and initializers would become complex, being dependent on three factors: delegation status, isolation of `self`, and the caller's context. The usual Sendable rules only depend on two.
868+
2. Requiring `convenience` for just one narrow use-case is not worth it.
869+
3. Static functions, using a factory pattern, can replace the need for initializers with non-Sendable arguments callable from anywhere.
870+
874871
## Effect on ABI stability
875872

876873
This proposal does not affect ABI stability.

0 commit comments

Comments
 (0)