Improving Sendable

I definitely agree with this.

Personally, I think that declaring Sendable via an unconstrained extension should only be done from outside the module, when the defining module has not yet adopted concurrency.

I'd like to tease apart your suggestions, because one is syntactic and the other is more semantic. The semantic suggestion addresses your concern about subclasses not knowing that they've been signed up to be Sendable by their superclass. If I take away the syntactic changes you're describing, it's:

class C2: @unchecked Sendable { } // sure, this is fine

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

class C4: C2, @unchecked Sendable { } // okay!

I like this direction: it makes the Sendable requirement obvious for subclasses, so one cannot accidentally violate sendability. I'll incorporate this in the next version of the proposal.

At this point, I don't know that I would go back and change Sendable and @Sendable. I think there are generalizations where this makes sense---for example, we could allow any marker protocol to also annotate function types---but we derive a lot of benefits from Sendable being a protocol and we don't really have a syntax for saying that an arbitrary function type conforms to a protocol. Much of the syntax you show is, to me, less understandable than "Sendable is a protocol and this is what it means".

Doug

5 Likes

Hi all, I'm about to revise this proposal based on feedback here and more experimentation with enabling Sendable checking in apps and seeing where the fallout is. One of the things we tripped over is that the limitations on inference of Sendable for public types is unfortunate. If I have something like this:

public struct Point {
  var x, y: Double
}

It doesn't get a Sendable conformance. We don't infer Sendable conformances for public types because we didn't want to make an implicit promise to other modules that the Sendable conformance exists, but there is a better model for this: scoped conformances. If we could infer the Sendable conformance but keep it internal to the module, then we would only need to explicitly write Sendable when we want to export that information to clients (and commit to it). It's more in line with, e.g., the synthesized member initializers of structs being internal.

I'll write it up as part of "version 2" of this pitch, but... what do you think?

Doug

18 Likes

I’m not sure whether to continue the discussion here, because it seems the Non-protocol type & declaration feature can be a more general one, and we’ve already come up with some other use cases. The impact of such syntax is broad and far beyond Sendable.

The most recent case I ran into is to add async implementation for some EventLoop-based protocols. Consider the following:

protocol Sender {
    associatedtype SendStatus
    func send(_ message: Codable, on eventLoop: EventLoop) -> EventLoopFuture<SendStatus>
}

protocol AsyncSender: Sender {
    func send(_ message: Codable, on eventLoop: EventLoop) async throws -> SendStatus
}

extension AsyncSender {
    func send(_ message: Codable, on eventLoop: EventLoop) -> EventLoopFuture<SendStatus> {
        let promise = eventLoop.makePromise(of: SendStatus.self)
        promise.completeWithTask { try await send(message) }
        return promise.futureResult
    }
}

If we want to add default func send(_ message: Codable) async throws -> SendStatus implementation to Sender, what should we do? With Non-protocol type, we can use:

extension Sender where Self: !AsyncSender {
    func send(_ message: Codable, on eventLoop: EventLoop) async throws -> SendStatus {
        try await send(message, on: eventLoop).get()
    }
}

Then both Sender and AsyncSender will get the same API surface.

Have all the issues with scoped conformances been resolved? If not, is it wise to put them on the critical path for Sendable?

1 Like

In my recollection, the main unresolved issues with scoped conformances are all about their interaction with runtime conformance lookup, e.g., when triggered via ‘as?’. Sendable dodges that issue because it can never be looked up at runtime, so it’s only the static resolution that matters.

It could still be a bad idea to sneak partial scoped conformances into the language this way. Or maybe it will provide a stepping stone for the full feature in the future.

Doug

3 Likes

Hi all,

Thinking about this more, my attempt at pulling together all of the Sendable-related fixes into a single proposal probably meant that individual issues didn't get enough attention. Unsafe pointers are [handled by SE-0331. There are a number of unresolved issues with Sendable that could be tackled separately. Here's the list I know of:

  • Implicit inference of Sendable could be better: inferring conditional conformance and using scoped conformances would provide a better model.
  • Keypaths and Sendable aren't working well: we need the notion of non-Sendable key paths (e.g., to refer to actor-isolated state), while making it clear when key paths can be Sendable. We may also want to have some notion of asynchronous key paths that can
  • Partial application of some functions, and references to global/non-isolated functions, should produce @Sendable functions in some cases. This needs to be explored.
  • Sendable with class types needs to be revisited. We likely want to require subclasses to restate Sendable conformance, decide if there are any cases where that conformance need not be @unchecked (e.g., all state is immutable and Sendable) and whether it can ever be inferred (same conditions as for removing @unchecked?).

My pitches have tackled a few of these, but not all of them, and not in enough depth. We'll need to resolve all of them to make Sendable usable.

Doug

4 Likes