Difficulty designing a static requirement due to Sendable & SE-0412

This thread has provided a couple of examples of why adding Sendable conformance can be a nuisance. I just want to note, though, that my impression of Sendable's origins and intent is that it's kind of like Copyable, Movable, etc: it seems it was expected that most things will be Sendable; that it's essentially the default. I wonder, if it were added now, would that in fact be expressed more strongly by making it opt-out instead of opt-in, i.e. ~Sendable?

I know that doesn't help practically, I just think it's an interesting retrospective. And may explain the "just make it Sendable" form responses.

It might have sense in some kind of mundane code, but it wouldn't fly in code bases that aim at providing the maximum service with the lowest level of constraint, i.e. library code. If Sendable were the default, libs would use ~Sendable and we'd be back at the beginning of the thread.

i really dislike the recent trend of spraying every POD type with Sendable. a lot of times these conformances have no immediate purpose, they are just being added preemptively in case your future self tries to use the type inside an actor method. most of them will never be used and just end up polluting the library’s API.

Fortunately, Sendable is automatically synthesised for internal types.

For public library types, not flagging a type as Sendable, when it can be, is a disservice to the library users: they aren't able to freely use the type in concurrent code, and when they face compiler warnings they have doubts about the library intent: is it an oversight, or is the type really not sendable? At best, the user contacts the library author. At worst, the user slams an @unchecked Sendable on the type (even when the library intent was indeed that the type wasn't Sendable).

We had plenty of opportunities to feel the lack of Sendable conformances with Foundation.

Sometimes, libs even go too far in their concurrency annotations :sweat_smile:

3 Likes

To be clear, I wasn't expressing my own opinion on this point. Just reflecting on how it seems Sendable was perceived by its authors, if not the Swift team more broadly. And they might have since changed their opinions anyway. But I think it's helpful to keep in mind w.r.t. how that thinking may have influenced how Sendable works.

The Xcode 15.3 betas are a fantastic opportunity for feedback. I can't speak for the Swift team obviously, but I think they are happy when the feedback validates the designs that went through evolution, which means that well-motivated feature requests do not introduce breaking change and can be considered at a controlled pace. We're indeed at the level of the feature request, not of the bug report.

3 Likes

Having tried to imagine how my feature request could be implemented, I think I understand now. A strict sub-typing relation is required when the compiler nicely considers a "sendable variant" of the explicit type during the assignment:

// ✅ Compiler knows that `any Sendable`
// is a subtype of `Any`, so this can fly:
static let x: Any = 1

// ✅ Compiler knows that `[any P & Sendable]`
// is a subtype of `[any P]`, so this can fly:
static let x: [any P] = [1]

// ❌ Compiler does not consider `MyContainer<any P & Sendable>`
// as a subtype of `MyContainer<any P>`, so a warning is required:
static let x: MyContainer<any P> = MyContainer(value: 1)

If I'm correct, I'd understand the hesitation of the Swift team to hard-code more special cases for arrays, dictionaries, optionals, etc.

How I was imagining the implementation

Considering a general assignment as below:

static let x: [type] = [expression]

The compiler could run the following algorithm when determining if there is a Sendable violation or not. Of course I'm not familiar with the compiler internals, and I'm probably violating many compiler layers - please bear with me: I just want to outline the principles of a possible solution.

For all conditional conformances of [type] to Sendable:
  Derive a [sendable subtype] from [type] and the conditional conformance.
  Can [expression] be assigned to a value of type [sendable subtype], without changing the type of literals?
    Yes -> return "no warning"
    No -> continue
Return "warn about Sendable violation"

Let's consider a first positive example:

static let x: [Any] = [1]

From conditional conformance of Array to Sendable when Element conforms to Sendable, consider the subtype [any Sendable].

Lock 1 to Int because Int fits Any.

static let x: [any Sendable] = [1 as Int] is valid -> no warning.

And now consider a negative example:

protocol P { }
protocol Q: P { }
struct MyContainer<T> {
    init(value: T) { ... }
}
extension MyContainer: Sendable where T == any Q { }

extension Int: P { }
extension Int64: P, Q { }

static let x: MyContainer<any P> = MyContainer(value: 1)

From conditional conformance of MyContainer to Sendable when T is any Q, consider the subtype MyContainer<any Q> (:warning: TODO: prove the subtype relation).

Lock 1 to Int because Int fits any P.

static let x: MyContainer<any Q> = MyContainer(value: 1 as Int) is not a valid assignment -> Sendable violation warning.

I don't think this is true. Sendable is inherent for many kinds of types, including value types composed of Sendable properties, immutable final classes, actor-isolated types (including actors and global-actor isolated classes/structs), etc. However, there's a ton of Swift code out there that uses mutable reference types which are not Sendable and will never become Sendable unless they are only ever used from a specific isolation domain such as the main actor.

It is not a goal for everything to become Sendable, and I hope that the acceptance of SE-0414 helps people realize that not everything should be marked as Sendable. From my perspective, the goal is specifically to mark the values that are thread safe as such, and in some cases, put in the additional work to make types thread safe when they are already used that way in practice.

I completely agree. Sendable is explicit documentation about whether or not a type is thread safe / safe to share across isolation boundaries. It has always been the case that users of libraries need to know whether or not the types they're using are thread safe, and Sendable conformances will make finding that information much easier.

I think this is actually a consequence of using extensions as a namespace of sorts. When the enclosing type became @MainActor-isolated, so did all of these constants. I wonder if the compiler should issue a warning if you've written a static let of Sendable type in an actor-isolated context with a fix-it to insert nonisolated. You can already access the variable as if it were nonisolated from within the module, but you may very well want that to be the case outside the module as well. Perhaps this is also something that the Clang importer can do by default, which would fix these cases with NSNotification.Name.

I can't emphasize this enough. I think that feedback from Xcode 15.3 / Swift 5.10 is critical ahead of Swift 6. Everybody writes code differently, which means that different people will discover different usability issues with strict concurrency checking. Providing feedback on the forums is also a great way to build a shared understanding amongst the community about which code patterns are concurrency safe and why, which was difficult to do prior to Swift 5.10 because the design for full data isolation wasn't complete. I appreciate all of the recent discussion threads on new concurrency diagnostics in 5.10 and potential strategies for mitigating false reports of data races. Please continue to discuss your experience using strict concurrency checking in Swift 5.10!

11 Likes

I had the same intuition.

I wonder if the compiler should issue a warning if you've written a static let of Sendable type in an actor-isolated context with a fix-it to insert nonisolated. You can already access the variable as if it were nonisolated from within the module, but you may very well want that to be the case outside the module as well.

This sounds very sensible. It's far too easy to lock an already-Sendable value inside a global actor by mistake.

For the record, I reported the same problem (with a luxury of details as why this is so wrong) in FB9801372 (in... 2021 :sweat_smile:). To me, this useless MainActor confinement is such a vexing inconvenience that it errs on the side of the library bug.

Perhaps this is also something that the Clang importer can do by default, which would fix these cases with NSNotification.Name.

As in "if the type is already Sendable, ignore the global actor of the enclosing type" ?

1 Like