Constraining an associatedtype to an existential protocol

I'm creating bindings for Windows Runtime APIs whose type system relies heavily on runtime dispatching interfaces that I map to existential protocols in Swift. As part of this, I have an abstraction that maps native pointer objects to a Swift representation, but I'm unable to represent an associatedtype constrained to an existential protocol:

protocol IUnknown {}
protocol IInspectable: IUnknown {}

protocol Projection {
    associatedtype Interface: any IUnknown //  error: type 'Self.Interface' constrained to non-protocol, non-class type 'any IUnknown'
    static func toSwift(_: UnsafeRawMutablePointer) -> Interface
}

class IInspectableProjection: Projection {
    typealias Interface = any IInspectable
    static func toSwift(_: UnsafeRawMutablePointer) -> any IInspectable
}

Is there a way to express this in Swift? If not, I'm curious if there are implementation limitations at play?

Existential types cannot conform to protocols today. To better understand what you’re trying to express, suppose I write a function that is generic over your protocol:

func f<T: P>(_ p: P) {
  var result = p.toSwift(…)
}

What is the (static) type of result, and what operations can I perform on this type? Is it just any IUnknown or is there more I should be able to do?

Certainly an existential type can witness an unconstrained associated type; by writing a requirement on the associated type, we constrain the types that can witness it while introducing new capabilities on the associated type inside the generic function.

1 Like

I believe you might have wanted the following instead (note the any was removed).

protocol Projection {
    associatedtype Interface: IUnknown
    static func toSwift(_: UnsafeRawMutablePointer) -> Interface
}

The reason is that IUnknown is the protocol, any IUnknown is used in contexts where a type is expected.

Associated types expect protocols, not types. You'll get the exact same error if you were trying to constrain it to any other types:

protocol Projection {
    associatedtype Interface: Int //  error: type 'Self.Interface' constrained to non-protocol, non-class type 'Int'
    static func toSwift(_: UnsafeRawMutablePointer) -> Interface
}

@code-per-day, I actually want the shape that I wrote, with the type erasure implied with the use of the existential. This allows me to cast a value of type any IInspectable to a value of type any IUnknown, which matches the semantic of the COM / WinRT type system that I'm creating bindings for. My implementation also makes the type to which "Interface" eventually gets bound private, such that I need the existential to refer to it.

@Slava_Pestov
I don't think my scenario involves having an existential type conform to a protocol. I can implicitly convert an any IInspectable to an any IUnknown, and call IUnknown methods on an instance of any IInspectable, so it would make sense to me that I could define that a protocol has an associated type of any IUnknown or more specific (such as any IInspectable).

(both IUnknown and IInspectable don't have associated types)

Do you need an associated type at all then? It sounds like the protocol method can be declared to return any IUnknown.

1 Like

I think what you're looking for is an "associated protocol" - there's a pretty heavy clue in that the name of your associated type is "Interface". Associated types aren't witnessed by interfaces; they are witnessed by concrete types.

For instance, Collection has an associated type Index. Every conformance to Collection must pick a single, concrete type to use as its Index - it could choose Int, for instance, but it couldn't choose a protocol like FixedWidthInteger. And we're talking about the protocol itself here; to be used as a constraint.

It has come up from time to time. I think it would be an interesting thing to explore, but right now it is not supported by Swift's generics system.

@Slava_Pestov , I still want to have a stronger type returned from that method from conformances (any IInspectable)

@Karl, that sounds right. An associated protocol would give me a way to implement it, but since I only use the protocol as an existential, I was hoping there would be a way to express this using associated types.

A couple thoughts:

  • Would it suffice to have a struct AnyIInspectable: IInspectable which wraps an any IInspectable and serves as IInspectableProjection.Interface?
  • Is it feasible to have Projection act as a superclass (and drop the associated type) so that toSwift(_:) could be an override, which in Swift is permitted to return a more-specific type than the overridden method?
  • If it were possible to witness a protocol requirement with an implementation which returns a more-specific type (though it isn’t possible today), would that satisfy the requirements here without an associated type?
3 Likes
  1. I can't implicitly convert between such an AnyIInspectable and an AnyIUnknown, whereas I can between any IInspectable and any IUnknown.
  2. Possibly actually, although my actual protocol has other associated types and this is a little farther than I am willing to go in my scenario. My current solution (no associatedtype constaint) works, it just sometimes requires casts because the type system doesn't know that the Interface value is any IUnknown-compatible.
  3. I think so, but I get a little lost in the jargon.

I'm satisfied that there's no way to express what I need in Swift currently. It's not a big deal for my scenario, I just wanted to check if I was missing something. Thanks to all of you who provided thoughts.

3 Likes

For (1), I was picturing that most APIs would still traffic in any IInspectable and any IUnknown to maintain the conversions, with the concrete AnyIInspectsble wrapper existing mostly as an implementation detail used only to provide a concrete associated type.

1 Like

I think what @Jumhyn is referring to is the ability to do this:

protocol Base {}
protocol Derived: Base {}

protocol P {
  func f() -> any Base
}

struct S: P {
  func f() -> any Derived { ... }
}

This is not supported today because the type of the witness must match the requirement exactly. But it's a possible future generalization because it would be safe to allow, since there's the obvious conversion from any Derived to any Base.

1 Like

Yes, that's exactly what I had in mind; thank you for expanding on my hand waving :slightly_smiling_face:. It's also worth noting that this sort of construction is supported for subclass overrides, which I was gesturing at in (2):

class B {
    func f() -> B {
        self
    }
}

class C: B {
    override func f() -> C { // ok, this is a valid override!
        self
    }
}
1 Like

And don't forget @objc protocol conformances which have their own rules! We accept this, but with a warning:

import Foundation

@objc protocol P {
  func f() -> AnyObject?
}

class C: P {
  func f() -> AnyObject { fatalError() }
}
1 Like

And the same for the arguments please:

protocol P {
    func b(v: any Derived)
}

struct S: P  {
    func b(v: any Base) {}
}

Another potential solution using the unofficial @_implements feature, though in cases where you're dealing with the narrower type it will potentially require type disambiguation to pick the right overload, if there's no other context to guide the selection :frowning:

protocol IUnknown {}
protocol IInspectable: IUnknown {}

protocol Projection {
    static func toSwift(_: UnsafeRawPointer) -> any IUnknown
}

class IInspectableProjection: Projection {
    @_implements(Projection, toSwift(_:))
    static func _Projection_toSwift(_ p: UnsafeRawPointer) -> any IUnknown {
        (toSwift as (UnsafeRawPointer) -> any IInspectable)(p)
    }

    static func toSwift(_: UnsafeRawPointer) -> any IInspectable {
        fatalError()
    }
}

func f(_ p: UnsafeRawPointer) {
    let _: any IInspectable = IInspectableProjection.toSwift(p) // 'any IInspectable' annotation needed here
}