-
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 2 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,342 @@ 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. | ||||||
| An implementation of a nonisolated protocol conformance | ||||||
| can still use global actor isolated state. | ||||||
| A conformance that needs global actor isolated state | ||||||
hborla marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| is called an *isolated* conformance. | ||||||
| When a conformance is isolated, | ||||||
| Swift prevents data races by ensuring that | ||||||
| the conformance is only used on the global 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"? |
||||||
| Isolated conformances are also 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 isolated conformance to `Equatable` in an extension: | ||||||
hborla marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
|
|
||||||
| ```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 called | ||||||
| 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 called through type parameters or `any` types. | ||||||
|
|
||||||
| #### 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. |
||||||
|
|
||||||
| ##### Generic Code | ||||||
|
|
||||||
| A conformance requirement to `Sendable` allows generic code to send parameter | ||||||
| values to concurrently-executing code. If generic code accepts non-`Sendable` | ||||||
| types, 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 requirement as long as the caller is on the same global | ||||||
hborla marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| actor that the conformance is isolated to. The following code has a protocol | ||||||
| `P`, a class `C` with a main-actor isolated conformance to `P`, and two | ||||||
| call-sites to a generic method that accepts `some P`: | ||||||
|
|
||||||
| ```swift | ||||||
| protocol P { | ||||||
hborla marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| func perform() | ||||||
| } | ||||||
|
|
||||||
| func perform(_ p: some P) { | ||||||
| p.perform() | ||||||
| } | ||||||
|
|
||||||
| @MainActor class C: P { ... } | ||||||
hborla marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
|
|
||||||
| Task { @MainActor in | ||||||
| let c = C() | ||||||
| perform(c) | ||||||
| } | ||||||
|
|
||||||
| Task { @concurrent in | ||||||
| let c = C() | ||||||
| perform(c) // Error | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| The above code calls `perform` | ||||||
| and provides an argument with a main-actor isolated conformance to `P`. | ||||||
| Calling `perform` from a main actor task | ||||||
| is safe because it matches the isolation of the conformance. | ||||||
| Calling `perform` from a concurrent task | ||||||
| results in an error, | ||||||
| because it would allow calling the main actor isolated implementation of `perform` | ||||||
| from outside the main actor. | ||||||
|
|
||||||
| ##### Dynamic Casting | ||||||
|
|
||||||
| Generic code can check whether a value conforms to a protocol | ||||||
hborla marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| through dynamic casting. | ||||||
| The following code has a protocol `P`, | ||||||
| and a method `performIfP` that accepts a parameter of type `Any` | ||||||
| which is dynamic cast to `any P` in the function body: | ||||||
hborla marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
|
|
||||||
| ```swift | ||||||
| protocol P { | ||||||
| func perform() | ||||||
| } | ||||||
|
|
||||||
| func performIfP(_ value: Any) { | ||||||
| if let p = value as? any P { | ||||||
| p.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 the global actor. | ||||||
hborla marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| If you declare a main-actor isolated conformance to `P` | ||||||
| and call `performIfP` with an instance of the conforming type, | ||||||
| the dynamic cast will only succeed | ||||||
hborla marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| when `performIfP` is called from the main actor: | ||||||
|
|
||||||
| ```swift | ||||||
| @MainActor class C: P { | ||||||
| func perform() { | ||||||
| print("C.perform") | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| Task { @MainActor in | ||||||
| let c = C() | ||||||
| performIfP(c) // Prints "C.perform" | ||||||
| } | ||||||
|
|
||||||
| Task { @concurrent in | ||||||
| let c = C() | ||||||
| performIfP(c) // Prints nothing | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| In the above code, | ||||||
| the call to `performIfP` from a main-actor isolated task | ||||||
| matches the isolation of the conformance, | ||||||
| so the dynamic cast succeeds. | ||||||
| The call to `performIfP` 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 called | ||||||
| through instances of conforming types and through 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 satisfy a conformance requirement to `Sendable` or `SendableMetatype`. | ||||||
|
|
||||||
| A conformance requirement to `Sendable` indicates | ||||||
| that instances may be passed across isolation boundaries and used concurrently: | ||||||
|
|
||||||
| ```swift | ||||||
| protocol P { | ||||||
| func perform() | ||||||
| } | ||||||
|
|
||||||
| func performConcurrently<T: P>(_ t: T) where T: Sendable { | ||||||
| Task { @concurrent in | ||||||
| t.perform() | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| The above code admits data races if the conformance to `P` is isolated, | ||||||
hborla marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| because the implementation of `perform` | ||||||
| may access global actor isolated state. | ||||||
| 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 C: P { ... } | ||||||
|
|
||||||
| let c = C() | ||||||
| performConcurrently(c) // Error | ||||||
| ``` | ||||||
|
|
||||||
| The above code results in an error | ||||||
| because the conformance of `C` to `P` is main-actor isolated, | ||||||
| which can't satisfy the `Sendable` requirement of `performConcurrently`. | ||||||
|
|
||||||
| Protocol requirements can also be called through metatype values. | ||||||
hborla marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| 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 P { | ||||||
| static func perform() | ||||||
| } | ||||||
|
|
||||||
| func performConcurrently<T: P>(n: Int, for: T.Type) async where T: SendableMetatype { | ||||||
| await withDiscardingTaskGroup { group in | ||||||
| for _ in 0..<n { | ||||||
| group.addTask { | ||||||
| T.perform() | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| Without a conformance to `SendableMetatype`, | ||||||
| generic code must only use metatype values in the current isolation domain. | ||||||
hborla marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| The following code results in an error | ||||||
| because the non-`Sendable` metatype `T` | ||||||
| is used from concurrent child tasks: | ||||||
|
|
||||||
| ```swift | ||||||
| protocol P { | ||||||
| static func perform() | ||||||
| } | ||||||
|
|
||||||
| func performConcurrently<T: P>(n: Int, for: T.Type) async { | ||||||
| await withDiscardingTaskGroup { group in | ||||||
| for _ in 0..<n { | ||||||
| group.addTask { | ||||||
| T.perform() // Error | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| 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 `P`: | ||||||
|
|
||||||
| ```swift | ||||||
| @MainActor class C: P { | ||||||
| static func perform() { /* use main actor state */ } | ||||||
| } | ||||||
|
|
||||||
| let items = performConcurrently(n: 10, for: C.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 { | ||||||
| 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.