-
Notifications
You must be signed in to change notification settings - Fork 201
Add a section on isolated conformances to the guide [SE-0470] #381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1ad8fee
e46557f
45826d2
1ecd2ba
d32afa1
834c63b
7a0fc92
f1f8c33
439e38b
7fc52b1
e53a81c
b17e906
6c7806a
9c98fe5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1591,6 +1591,370 @@ You can also use an unavailable conformance | |||||
| to suppress implicit conformance to a protocol, | ||||||
| as discussed in <doc:Protocols#Implicit-Conformance-to-a-Protocol>. | ||||||
|
|
||||||
| ## Isolated Protocol Conformances | ||||||
|
|
||||||
| Protocols that are nonisolated | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's either define "nonisolated protocol" if it's a new concept being introduced, or find a way to refer to normal protocol conformance that doesn't sound like a new term. |
||||||
| can be used from anywhere in a concurrent program. | ||||||
| A conformance to a nonisolated protocol can be isolated | ||||||
| to a global actor, which allows the implementation to | ||||||
| access actor isolated state synchronously. | ||||||
| This is called an *isolated conformance*. | ||||||
| When a conformance is isolated, | ||||||
| Swift prevents data races by ensuring that | ||||||
| the conformance is only used on the actor | ||||||
| that the conformance is isolated to. | ||||||
|
|
||||||
| ### Declaring an Isolated Conformance | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You probably don't need a heading here after just one paragraph. |
||||||
|
|
||||||
| You declare an isolated conformance | ||||||
| by writing the global actor attribute before the protocol name | ||||||
| when you implement the conformance. | ||||||
| The following code example declares | ||||||
| a main-actor isolated conformance to `Equatable` in an extension: | ||||||
|
|
||||||
| ```swift | ||||||
| @MainActor | ||||||
| class Person { | ||||||
| var id: Int | ||||||
| } | ||||||
|
|
||||||
| extension Person: @MainActor Equatable { | ||||||
| static func ==(lhs: Person, rhs: Person) -> Bool { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To match TSPL style elsewhere:
Suggested change
Likewise below; marking it just here. |
||||||
| return lhs.id == rhs.id | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| This allows the implementation of the conformance | ||||||
| to use global actor isolated state | ||||||
| while ensuring that state is only accessed | ||||||
| from within the actor. | ||||||
|
Comment on lines
+1628
to
+1631
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's walk through the code listing more step-by-step in its explanation paragraph. For example:
Expand "This allows" so the reader doesn't have to guess what "this" refers to. Here, probably "This isolated conformance allows"? |
||||||
|
|
||||||
| ### Inferring an Isolated Conformance | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The developer reading TSPL isn't doing the inference, the compiler is. Maybe "Inferred" or "Using Inferred" in the heading? |
||||||
|
|
||||||
| > Note: | ||||||
| > Isolated conformance inference is an upcoming language feature. | ||||||
| > To enable it in current language modes of Swift, | ||||||
| > use the feature identifier `InferIsolatedConformances`. | ||||||
|
|
||||||
| Isolated conformances are inferred | ||||||
| for global actor isolated types. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's discuss "global actor isolated types" with an editor, find a phrasing that avoids the noun pile, and add an entry to the TSPL style guide. Hyphenating like you did below (global-actor-isolated type) works, but could be hard to read. Expanding it to "types that are isolated to a global actor" is likely to be too wordy when used more than once or twice. |
||||||
| The following code example declares a conformance to `Equatable` | ||||||
| for a main-actor isolated class, | ||||||
| and Swift infers main-actor isolation for the conformance: | ||||||
|
|
||||||
| ```swift | ||||||
| @MainActor | ||||||
| class Person { | ||||||
| var id: Int | ||||||
| } | ||||||
|
|
||||||
| // Inferred to be a @MainActor conformance to Equatable | ||||||
| extension Person: Equatable { | ||||||
| static func ==(lhs: Person, rhs: Person) -> Bool { | ||||||
| return lhs.id == rhs.id | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the listing above just a shorter (implicit) way to spell the listing that came before it? Let's call out the difference, or lack of difference. |
||||||
| You can opt out of this inference for a global-actor-isolated type | ||||||
| by explicitly declaring that a protocol conformance is nonisolated. | ||||||
| The following code example declares | ||||||
| a nonisolated conformance to `Equatable` in an extension: | ||||||
|
|
||||||
| ```swift | ||||||
| @MainActor | ||||||
| class Person { | ||||||
| let id: Int | ||||||
| } | ||||||
|
Comment on lines
+1666
to
+1669
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Within a running example, each code listing doesn't need to repeat code that's unchanged. In most of the running examples, a single piece of code is built up across multiple code listings. That style decision comes, in part, from the pre-DocC build system that literally concatenated code listings (whose names were the same) into one file when compiling and testing. The consequence of that style is that TSPL often calls out in prose things like "here's another version of |
||||||
|
|
||||||
| extension Person: nonisolated Equatable { | ||||||
| nonisolated static func ==(lhs: Person, rhs: Person) -> Bool { | ||||||
| return lhs.id == rhs.id | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ### Data-Race Safety for Isolated Conformances | ||||||
|
|
||||||
| Swift prevents data races for isolated conformances | ||||||
| by ensuring that protocol requirements are only used | ||||||
| on the global actor that the conformance is isolated to. | ||||||
| In generic code, | ||||||
| where the concrete conforming type is abstracted away, | ||||||
| protocol requirements can be used through type parameters or `any` types. | ||||||
|
Comment on lines
+1683
to
+1685
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found this sentence hard to understand until I started reading the code listing. By type parameters, I think this is referring to generic type parameters, right? TSPL uses both terms, but mostly uses "type parameters" in the Generics chapter — in this context, it might be worth spelling out. TSPL uses the term "boxed protocol types" rather than " |
||||||
|
|
||||||
| #### Using Isolated Conformances | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this heading here and at this level? Level 4 headings are allowed, but should be rare. They don't render especially well and often indicate there's a better, less deeply nested way, that we can organize the content. |
||||||
|
|
||||||
| A conformance requirement to `Sendable` | ||||||
| allows generic code to send parameter values to concurrently-executing code. | ||||||
| If generic code accepts non-`Sendable` types, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid using code voice as an English word, or part of a word.
Suggested change
|
||||||
| then the generic code can only use the input values | ||||||
| from the current isolation domain. | ||||||
| These generic APIs can safely accept isolated conformances | ||||||
| and call protocol requirements | ||||||
| as long as the caller is on the same global actor | ||||||
| that the conformance is isolated to. | ||||||
| The following code has a protocol `Dancer`, | ||||||
| a class `Ballerina` with a main-actor isolated conformance to `Dancer`, | ||||||
| and calls to the `Dancer.perform` requirement | ||||||
| from a main-actor task and a concurrent task: | ||||||
|
|
||||||
| ```swift | ||||||
| protocol Dancer { | ||||||
| func perform() | ||||||
| } | ||||||
|
|
||||||
| func startRecital(_ dancer: some Dancer) { | ||||||
| dancer.perform() | ||||||
| } | ||||||
|
|
||||||
| @MainActor class Ballerina: @MainActor Dancer { ... } | ||||||
|
|
||||||
| Task { @MainActor in | ||||||
| let ballerina = Ballerina() | ||||||
| startRecital(ballerina) | ||||||
|
|
||||||
| let dancer: any Dancer = ballerina | ||||||
| dancer.perform() | ||||||
| } | ||||||
|
|
||||||
| Task { @concurrent in | ||||||
| let ballerina = Ballerina() | ||||||
| startRecital(c) // Error | ||||||
|
|
||||||
| let dancer: any Dancer = ballerina // Error | ||||||
|
Comment on lines
+1724
to
+1726
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there's a way to briefly state the error, let's include that in the comment. Not all |
||||||
| dancer.perform() | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| Calling `Dancer.perform` in generic code and on an `any Dancer` type | ||||||
| from a main actor task | ||||||
| is safe because it matches the isolation of the conformance. | ||||||
| Calling `Dancer.perform` in generic code and on an `any Dancer` type | ||||||
| from a concurrent task results in an error, | ||||||
| because it would allow calling the main actor isolated implementation | ||||||
| of `Dancer.perform` from outside the main actor. | ||||||
|
|
||||||
| Abstract code that uses type parameters and `any` types | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't use the term "abstract code" elsewhere in TSPL — let's see if there's an existing term we can use here. |
||||||
| can check whether a value conforms to a protocol | ||||||
| through dynamic casting. | ||||||
|
Comment on lines
+1739
to
+1741
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This new part of the discussion needs a clearer connection to the current topic. How is this different from the existing discussion of |
||||||
| The following code has a protocol `Dancer`, | ||||||
| and a method `performIfDancer` that accepts a parameter of type `Any` | ||||||
| which is dynamically cast to `any Dancer` in the function body: | ||||||
|
Comment on lines
+1742
to
+1744
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be clearer to introduce the code listing with what details I should watch out for while reading it. For example here, I think the salient point is the |
||||||
|
|
||||||
| ```swift | ||||||
| protocol Dancer { | ||||||
| func perform() | ||||||
| } | ||||||
|
|
||||||
| func performIfDancer(_ value: Any) { | ||||||
| if let dancer = value as? any Dancer { | ||||||
| dancer.perform() | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| Isolated conformances are only safe to use | ||||||
| when the code is running on the global actor | ||||||
| that the conformance is isolated to, | ||||||
| so the dynamic cast only succeeds | ||||||
| if the dynamic cast occurs on that global actor. | ||||||
|
Comment on lines
+1761
to
+1762
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to be the critical point — let's lead with it. |
||||||
| For example, if you declare a main-actor isolated conformance to `Dancer` | ||||||
| and call `performIfDancer` with an instance of the conforming type, | ||||||
| the dynamic cast will succeed | ||||||
| when `performIfDancer` is called in a main actor task, and it | ||||||
| will fail when `performIfDancer` is called in a concurrent | ||||||
| task: | ||||||
|
|
||||||
| ```swift | ||||||
| @MainActor class Ballerina: @MainActor Dancer { | ||||||
| func perform() { | ||||||
| print("Ballerina.perform") | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| Task { @MainActor in | ||||||
| let ballerina = Ballerina() | ||||||
| performIfDancer(ballerina) // Prints "Ballerina.perform" | ||||||
| } | ||||||
|
|
||||||
| Task { @concurrent in | ||||||
| let ballerina = Ballerina() | ||||||
| performIfDancer(ballerina) // Prints nothing | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| In the above code, | ||||||
| the call to `performIfDancer` from a main-actor isolated task | ||||||
| matches the isolation of the conformance, | ||||||
| so the dynamic cast succeeds. | ||||||
| The call to `performIfDancer` from a concurrent task | ||||||
| happens outside the main actor, | ||||||
| so the dynamic cast fails and `perform` is not called. | ||||||
|
|
||||||
| #### Restricting Isolated Conformances in Concurrent Code | ||||||
|
|
||||||
| Protocol requirements can be used | ||||||
| through instances of conforming types and through | ||||||
| instances of the conforming types themselves, | ||||||
| called *metatype values*. | ||||||
| In generic code, | ||||||
| a conformance requirement to `Sendable` or `SendableMetatype` | ||||||
| tells Swift that an instance or metatype value is safe to use concurrently. | ||||||
|
Comment on lines
+1802
to
+1804
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This reads like a definition of |
||||||
| To prevent isolated conformances from being used outside of their actor, | ||||||
| a type with an isolated conformance | ||||||
| can't be used as the concrete generic argument for a type | ||||||
| parameter that requires a conformance | ||||||
| to `Sendable` or `SendableMetatype`. | ||||||
|
|
||||||
| A conformance requirement to `Sendable` indicates | ||||||
| that instances may be passed across isolation boundaries and used concurrently: | ||||||
|
|
||||||
| ```swift | ||||||
| protocol Dancer { | ||||||
| func perform() | ||||||
| } | ||||||
|
|
||||||
| func performConcurrently<D: Dancer>(_ dancer: D) where D: Sendable { | ||||||
| Task { @concurrent in | ||||||
| dancer.perform() | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| The above code would admit data races | ||||||
| if the conformance to `Dancer` was isolated, | ||||||
| because the implementation of `perform` | ||||||
| may access global actor isolated state. | ||||||
|
Comment on lines
+1826
to
+1829
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm having a hard time with this sentence, probably because of the irrealis mood on the verbs (would admit, was isolated, may access). Are you saying that the code above is safe, and then describing why by telling me about something that can't happen? Or are you describing a scenario where the code has possible data races? |
||||||
| To prevent data races, | ||||||
| Swift prohibits using an isolated conformance | ||||||
| when the type is also required to conform to `Sendable`: | ||||||
hborla marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| ```swift | ||||||
| @MainActor class Ballerina: @MainActor Dancer { ... } | ||||||
|
|
||||||
| let ballerina = Ballerina() | ||||||
| performConcurrently(ballerina) // Error | ||||||
| ``` | ||||||
|
|
||||||
| The above code results in an error | ||||||
| because the conformance of `Ballerina` to `Dancer` is main-actor isolated, | ||||||
| which can't satisfy the `Sendable` requirement of `performConcurrently`. | ||||||
|
|
||||||
| Static and initializer protocol requirements | ||||||
| can also be called through metatype values. | ||||||
| A conformance to Sendable on the metatype type, | ||||||
| such as `Int.Type`, | ||||||
| indicates that a metatype value is safe | ||||||
| to pass across isolation boundaries and used concurrently. | ||||||
| Metatype types can conform to `Sendable` | ||||||
| even when the type does not conform to `Sendable`; | ||||||
| this means that only metatype values are safe to share in concurrent code, | ||||||
| but instances of the type are not. | ||||||
|
|
||||||
| In generic code, | ||||||
| a conformance requirement to `SendableMetatype` | ||||||
| indicates that the metatype of a type conforms to `Sendable`, | ||||||
| which allows the implementation to share metatype values in concurrent code: | ||||||
|
|
||||||
| ```swift | ||||||
| protocol Dancer { | ||||||
| init() | ||||||
| func perform() | ||||||
| } | ||||||
|
|
||||||
| func performConcurrently<D: Dancer>(n: Int, for: D.Type) async where T: SendableMetatype { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the
Suggested change
|
||||||
| await withDiscardingTaskGroup { group in | ||||||
| for _ in 0..<n { | ||||||
| group.addTask { | ||||||
| let dancer = T.init() | ||||||
| dancer.perform() | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| Without a conformance to `SendableMetatype`, | ||||||
| generic code must only use metatype values from the current isolation domain. | ||||||
| The following code results in an error | ||||||
| because the non-`Sendable` metatype `D` | ||||||
| is used from concurrent child tasks: | ||||||
|
|
||||||
| ```swift | ||||||
| protocol Dancer { | ||||||
| init() | ||||||
| func perform() | ||||||
| } | ||||||
|
|
||||||
| func performConcurrently<D: Dancer>(n: Int, for: D.Type) async { | ||||||
| await withDiscardingTaskGroup { group in | ||||||
| for _ in 0..<n { | ||||||
| group.addTask { | ||||||
| let dancer = D.init() // Error | ||||||
| dancer.perform() | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| Note that `Sendable` requires `SendableMetatype`, | ||||||
| so an explicit conformance to `SendableMetatype` is only necessary | ||||||
| if the type is non-`Sendable`. | ||||||
|
Comment on lines
+1903
to
+1905
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This information seems like it should come earlier. |
||||||
|
|
||||||
| Types with isolated conformances can't satisfy | ||||||
| a `SendableMetatype` generic requirement. | ||||||
| Swift will prevent calling `createParallel` | ||||||
| with a type that has an isolated conformance to `Dancer`: | ||||||
|
|
||||||
| ```swift | ||||||
| @MainActor class Ballerina: @MainActor Dancer { | ||||||
| init() { /* use main actor state */ } | ||||||
| func perform() { /* use main actor state */ } | ||||||
| } | ||||||
|
|
||||||
| let items = performConcurrently(n: 10, for: Ballerina.self) // Error | ||||||
| ``` | ||||||
|
|
||||||
| ##### Protocols That Require `Sendable` or `SendableMetatype` | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can't use H5 or code voice in headings. |
||||||
|
|
||||||
| <!-- XXX: Can't use code voice in headings --> | ||||||
|
|
||||||
| Protocols can directly require that | ||||||
| conforming types also conform to `Sendable` or `SendableMetatype`: | ||||||
|
|
||||||
| ```swift | ||||||
| public protocol Error: Sendable {} | ||||||
|
|
||||||
| public protocol ModelFactory: SendableMetatype { | ||||||
| static func create() -> Self | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| Note that the `Sendable` protocol requires `SendableMetatype`; | ||||||
| if an instance of a conforming type is safe to share across concurrent code, | ||||||
| its metatype must also be safe to share: | ||||||
|
|
||||||
| ```swift | ||||||
| public protocol Sendable: SendableMetatype {} | ||||||
| ``` | ||||||
|
|
||||||
| If a protocol requires `Sendable`, | ||||||
| then any use of the protocol | ||||||
| can freely send instances across isolation boundaries. | ||||||
| If a protocol requires `SendableMetatype`, | ||||||
| then uses of metatypes in generic code can cross isolation boundaries. | ||||||
| In both cases, | ||||||
| Swift prevents declaring an isolated conformance, | ||||||
| because generic code can always call requirements concurrently. | ||||||
|
|
||||||
| ```swift | ||||||
| @MainActor | ||||||
| enum MyError: @MainActor Error {} // Error | ||||||
| ``` | ||||||
|
|
||||||
| <!-- | ||||||
| LEFTOVER OUTLINE BITS | ||||||
|
|
||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.