Prevent an extra Sendable conformance from missing a protocol requirement?

Hello,

I was wondering if it would be a good idea or not to let a witness specify an extra Sendable conformance without missing a requirement. Or maybe I'd like T & Sendable to be a subtype of T, I don't quite know what I'm after.

Let me explain.

Given a protocol with a requirement that involves another protocol:

public protocol Edible { }
public protocol Glutton {
  static var favoriteFoods: [any Edible] { get }
}

A client may face compiler warnings due to the lack of Sendable conformance of the witness:

struct Honey: Edible, Sendable { }

struct Bear: Glutton {
    // Static property 'favoriteFoods' is not concurrency-safe
    // because non-'Sendable' type '[any Edible]' may have
    // shared mutable state.
    static let favoriteFoods: [any Edible] = [Honey()]
}

Possible workarounds for the client are to make the property computed, or nonisolated(unsafe):

// Possible workaround 1
struct Bear: Glutton {
    static var favoriteFoods: [any Edible] { [Honey()] }
}

// Possible workaround 2
struct Bear: Glutton {
    // Safe because Honey is Sendable, but the compiler can't see it
    nonisolated(unsafe) static let favoriteFoods: [any Edible] = [Honey()]
}

(There are other workarounds that modify the protocol itself, such as isolating the static requirement, but here the requirement is nonisolated and I'm looking after making the life of the client easier in this specific case).

I was wondering if we could allow the client to help the compiler by explicitly specifying the Sendable conformance of the witness:

// Currently fails to compile with:
// Type 'Bear' does not conform to protocol 'Glutton'
struct Bear: Glutton {
    static let favoriteFoods: [any Edible & Sendable] = [Honey()]
}

Benefits:

  • Sendable is a marker protocol that has no impact of the memory layout: [any Edible & Sendable] and [any Edible] are strictly identical at runtime. The requirement is indeed fulfilled, right?

  • In the case of static requirements, some clients have good reasons to prefer to use a stored property instead of a computed one.

    Some may even write it as below (but I'm afraid this creates a new array under the hood):

    // Painful Glutton conformance, but compiler is happy
    struct Bear: Glutton {
        private static let _favoriteFoods: [any Edible & Sendable] = [Honey()]
        static var favoriteFoods: [any Edible] { _favoriteFoods }
    }
    
  • There's no unsafe construct: the code will stop compiling if the list of favorite foods is extended with a non-Sendable type in the future.

Could this fly? Does it address a real need, according to your experience?

I also had this example which might be the same problem (not 100% sure).

protocol MyProtocol: Identifiable {
    func foo(with id: Self.ID) async
}

@MainActor
struct MyStruct: MyProtocol {
    let id = UUID()
    
    // Non-sendable type 'Self.ID' in parameter of the protocol requirement
    // satisfied by main actor-isolated instance method 'foo(with:)' cannot
    // cross actor boundary
    func foo(with id: UUID) async {
        
    }
}
1 Like

Given that Sendable conformance, specially on protocols/basic types, has a tendency to spread virally across the codebase, sometimes without the need, I would say that adding more freedom on implementation is valuable, yet I am not sure if there are any issues it might bring to sendability checks on the protocol itself.