Improving Sendable

Hey all,

We've combined @Andrew_Trick's pitch to remove Sendable conformances from the unsafe pointers with a couple of other adjustments to Sendable that we think would be useful, now that we have a bit more experience with it. The proposal itself has a few different pieces to make Sendable more complete and more robust:

  • Remove Sendable conformances from the unsafe pointer and key path types
  • Infer Sendable types for some key path literals
  • Introduce a syntax to specify explicitly that a type is non- Sendable
  • Extend Sendable inference to generic types via implicit conditional conformances
  • Introduce a compile-time flag that requires all public types to be explicitly annotated as Sendable or non- Sendable

Here is the full proposal.

Doug

17 Likes

This is the kind of thing that’s tricky, because how a generic parameter is used can affect whether or not it matters for Sendable-ness. It looks like the proposal iterates over each stored property type and copies its conditions for Sendability over to the type. There are a few ways that can (theoretically) be wrong:

  • The type should not be Sendable. In this case the newly proposed syntax can be used to deny the conformance altogether.

  • The type should be Sendable, but with fewer conditions than what was inferred. This is a form of “unsafe Sendable”.

  • The type should be Sendable, but there are more conditions than what’s inferred (maybe this is futureproofing for a library that might cache something but doesn’t today).

Maybe the inferred conformance should only be added if there’s not an explicit one? (Maybe that’s the plan already, but I didn’t spot it in the proposal.)

Could there be an @explicitlyNotConform(to:) attribute that eliminates auto conformance for any protocol? @unavailable and the proposed one looks weird because we typically specify protocol conformance after :, and any non-s will lead to confusion.

My proposed syntax:

@explicitlyNotConform(to: Sendable, Equatable)
enum MagicEnum {
  // ...
}

Specially, if a type is marked as explicitly not conforming to a protocol, auto conformance with other protocols that’s based on the “removed” protocol will also go away.

3 Likes

Yes, exactly. It's inferring generic constraints from the structural check over stored property types.

Oh, that's definitely what we meant to say! A conformance will only be inferred if there is not explicit conformance for that type. I'll improve the wording here, thanks!

Doug

3 Likes

There could be, yes. Any solution we pick here should work equally well for Sendable or any other inferred conformance.

An attribute on the type has some advantages, because you don't end up having bits in the conformance that mean "doesn't conform". It's also easier to implement, not that that should sway us overly much. Still, this attribute feels pretty verbose to me, although I don't have a better suggestion right now.

Doug

Any thought regarding rolling this into the library evolution flag rather than creating yet another language dialect (or, at least, making this a prerequisite for library evolution)? The rationale for this flag emphasizes libraries (as indeed does the design since it is about requiring public types to be annotated). Libraries distributed as binaries definitely can't be audited by their clients, while it's not prohibitive to do so for clients of source-available libraries.

1 Like

Could this be an unavailable NonSendable typealias?

Or a general feature, where Non-prefixed protocols are automatically recognized by the compiler?

The ! prefix would probably be considered too subtle:

public typealias NonSendable = Any & !Sendable

I see the reasoning behind using KeyPath & Sendable, and it makes sense to me. But it has an implication of changing type from a class type to an existential. And existentials do not satisfy superclass and protocol constraints, when used as generic arguments.

If keypath literals were typed as existentials at compile time, what would be their concrete type in the runtime (observable through type(of:))? Can we use that type at compile time as well?

Alternatively we can use opaque types to hide concrete type as an implementation detail, but I’m don’t see any benefits in this.

Overall, much valuable fine-tune and enhancement for Sendable conditions.

IIRC, as we've already accepted SE-290 : Unavailability Condition, it's better to extend @unavailable as equivalent of @available(*,unavailable) for this scenario as alternative section mentioned in the proposal. TBH, it looks more natural and intuitive to write and read.

IIRC, availability of a typealias controls when you can utter that typealias’s name, not when the thing using that typealias is available.

Interesting idea! My concern here is that very few libraries enable library evolution (because most don't care about ABI stability), so we'd be focusing the benefits of only a small part of the community.

I definitely hear you about the "creating another language dialect" issue, though.

Yeah, I'm coming around to this conclusion as well. It feels like it's in line with @unchecked Sendable and still leaves this as a step toward the more general availability syntax for conformances.

Doug

1 Like

This is fair, but I wonder how much of that is going to be inherent to the friction of having to enable a dialect via a compiler flag.

If the "not Sendable" annotation can be exposed as a garden variety conformance, then I'd expect that there are linters or other tools already out there (DocC even?) that can, with minimal work, be extended to ensure that every public type conforms to Sendable or "not Sendable." My hunch is that users conscientious enough to benefit from switching on a compiler flag would be just as likely to reach for such a tool and could reap most of the benefits without this being baked in as a dialect. I could be proven wrong though.

1 Like

I think it's great to add a way to disable automatic conformance! I strongly agree that this makes more sense as a type attribute than part of the inheritance clause.

Although I don't think there's any precedent for it, I could also see it as a compiler directive since its purpose (if I understand correctly) is to tell the compiler not to synthesize conformance.

So something like*

@disableSynthesis(Sendable, Equatable)
struct TaskLocalString {
  // ...
}

or

#noSynthesize Sendable, Equatable
struct TaskLocalString {
  // ...
}

(Though it's been pointed out to me that "synthesis" may be too arcane of a term for general use.) Regardless of the exact verbiage, though, I think this is the right place to put the annotation.


*Understanding that Equatable is not part of the pitch here; just thinking of the future.

2 Likes

If key paths are to not be sendable, does that mean it's not possible to define APIs that take a key path to use it later in an asynchronous context? As a toy example:

toggle(\.isEnabled, every: .seconds(1))

Edit: I guess maybe this isn't a problem if everything's @MainActor?

Yes it would be, according to my understanding. Your toggle function would accept KeyPath<...> & Sendable: swift-evolution/nnnn-sendable-inference.md at sendable-inference · DougGregor/swift-evolution · GitHub

[...] we can reflect the result in the type of the key path literal by making it a composition of the key path type and Sendable , such as KeyPath<Person, String> & Sendable .

The key path literal you’ve used in the example will be typed as Sendable. It takes some effort to get a non-sendable one - it needs to be a subscript key path, capturing a non-Sendable index value.

Even if you get a non-Sendable key path, I think indeed if you don’t leave @MainActor (or any other actor), it should be fine.

But if you want to round-trip it, then indeed it is not possible. But that is not a keypath-specific issue, this is a general limitation of the
Sendable. Safe round-trip requires much more advanced type system features - see Preventing data races using dependent types for example.

1 Like

Great improvements! I’m really fond of the conformance-opt-out syntax.


In my opinion, the proposal rightly uses @available — it’s well established and extensible. The @unavailable syntax, though, is novel and seems more like a future direction rather than an alternative.

In my own environment, it's a whole lot easier to roll out the use of a new compiler flag (which you can add as an option globally) than it would be to introduce a new tool like a linter. I also think the compiler can do a little bit better than a linting tool because it actually understands the requirements. For example, I'm experimenting with Fix-Its that provide conditional conformances (pull request here):

public struct X<T, U> {
  var array: [(T, U)] = []
}

// suggest a Fix-It with the following conditional conformance to Sendable
extension X: Sendable where T: Sendable, U: Sendable { }

Maybe neither of those are important enough to justify putting this in the compiler. Perhaps even if it should go in the compiler, it doesn't belong in the proposal because compiler flags have generally been handled outside of evolution. I'm on the fence here beyond wanting to make it easy for folks to do these Sendable audits, but not wanting to create a dialect.

[EDIT: I added a link to a pull request showing how this works]

Doug

2 Likes

@beccadax noted that we already have a syntax that indicates that the Sendable conformance is unavailable:

@available(*, unavailable)
extension MyPoint: Sendable { ... }

This already works in the compiler to disable Sendable inference for non-public structs and enums, so I'm going to take the new syntax out of this proposal and leave it for a later one if we want to optimize things.

Doug

2 Likes

Neither this proposal nor SE-0302 comments on the inheritability of Sendable by subclasses. I am concerned that the implied “expected” behavior is problematic. Classes are the only tool available to Swift programmers who want to author concurrency-safe reference types with synchronous methods.

While SE-0302 requires a Sendable class be final unless its sendability is marked @unchecked, a subclass could be defined very far away from its superclass, perhaps even in a different module. Or the conformance could be retroactively added in an extension by a third party. @unchecked might not provide a very visible signal to the subclass author that they must do extra work to maintain the superclass’s promise of Sendability. And declaring Sendable conformance in an extension seems generally unwise, except in the specific case of conditioning conformance on the sendability of generic parameters.

Aesthetically and pedagogically, I find the existence of both : Sendable and @Sendable unfortunate. If we address the above concerns by adding even more rules to Sendable that apply only to class types, it only increases the difficulty of explaining why the Sendable protocol behaves so uniquely.

I would like to re-evaluate modeling Sendable as a protocol, and propose an alternative in which sendable exists as a declaration modifier like public or async:

sendable struct S { }

typealias SendableClosure = () sendable -> Void

public final sendable class C { }

sendable class C2 { } // error: must use `@unchecked sendable`

class C3 : C2 { } // error: subclass of a sendable class must also be explicitly marked sendable

sendable protocol P { } // error: protocols cannot be sendable

sendable extension E { } // error: extensions cannot be sendable

var foo: sendable Any // but variables can be constrained to hold only sendable values

sendable actor A { } // error: actors are always sendable

sendable struct S2<T1, T2> { } // S2 is sendable off T1 and T2 are sendable
Terms of Service

Privacy Policy

Cookie Policy