Dynamically conforming a type to a protocol at runtime?

I'm pretty sure what I wrote in the title is impossible, but I'm hoping to find a solution to a problem and I think that the title sums it up well enough.

I have some protocol that defines a method:

protocol P {
    func p()
}

In my project, there will be various types that conform to P, and they will primarily be used as values of type any P. A type that conforms to P might be created, immediately type-erased to any P, then passed around as any P for the rest of its life. It's important that types that conform to P are (almost) only accessed as any P, since the types themselves are an implementation detail that should be hidden and easily swappable.

There are also a few additional protocols that inherit from P, that represent additional functionality that a type that conforms to P might want to implement. Types that conform to P can also conform to any number of these other protocols.

protocol PA: P {
    func a()
}
protocol PB: P {
    func b()
}

struct ImplP: P {
    func p() {}
}
struct ImplPA: PA {
    func p() {}
    func a() {}
}
struct ImplPAandPB: PA, PB {
    func p() {}
    func a() {}
    func b() {}
}

To access the functionality of these additional protocols, a value of type any P is conditionally downcast to the desired existential type.

func tryToCallA(p: any P) {
    if let pa = p as? any PA {
        pa.a()
    }
}

This system works great for types where I know what functionality they have at compile time and it feels very elegant. Unfortunately, there is a case where I need to decide which functionality a value of P has a runtime, when creating the value. (Its functionality is determined by data returned from a server).

First, before creating the value, data will be retrieved from a server that determines which functionality it supports. Then, a type that implements the proper protocols should be created and cast to any P.

One solution that I considered and rejected was creating separate types that conform to P for each possible set of functionality.

struct ServerP: P { ... }
struct ServerPA: PA { ... }
struct ServerPB: PB { ... }
struct ServerPAandPB: PA, PB { ... }

Depending on the data returned from the server, one of these types would be created, and cast to any P.

This solution isn't really practical, since the number of ServerX types needed grows exponentially as more protocols that inherent from P are added. In the actual project, there would be more than 2.

This is where the title comes in. Ideally, I could create only 1 ServerP type, that at initialization I specify which functionality it should implement based on the network data. The type somehow stores that info as fields, maybe optional function types.

struct ServerP: ?? {
    func p() // should always implement `P`
    let a: Optional<() -> Void> // might or might not implement `PA`, only known at runtime.
    let b: Optional<() -> Void>
}

Hopefully that makes sense.
When it is cast to a type like any PA, it checks whether it has an a() method or something, then succeeds or fails based on that. Unfortunately, I'm 99% sure this isn't possible with swift's type system - I can't think of any way of achieving it. Maybe there's some dirty trick with objective-c? But I'd rather stick to pure swift if possible.

Fundamentally I feel like this should be possible, but idk if theres any nice way to do it with protocols.

Any ideas?

Would it be feasible to do something like this by manually implementing e.g. AnyPA and AnyPB type erasers that look like:

struct AnyPA: PA {
  var _a: () -> Void
  init(a: @escaping () -> Void) {
    self._a = a
  }
  
  func a() {
    _a()
  }
}

protocol P {
  func f()
  var a: any PA { get }
  var b: any PB { get }
}

struct ServerP: P {
  func p() { ... }
  let a: any PA?
  let b: any PB?

  init(a: @escaping () -> Void, b: @escaping () -> Void) {
    a = AnyPA(a: a)
    b = AnyPB(b: b)
  }
}

and then rather than casting as? PA you'd do if let a = p.a to extract the implementation dynamically.

In the example you wrote, there are 3 protocols. P; PA, which inherits from P; and PB, which also inherits from P. You're trying to dynamically construct a type that will conform to P and may conform to A and/or B. I think you could do it this way.

struct AWrapper<T: P>: PA { 
  var wrappedValue: T
  var _a: () -> Void
  func p() { wrappedValue.p() }
  func a() { _a() }
}

extension AWrapper: PB where T: PB {
  func b() { wrappedValue.b() }
}

struct BWrapper<T: P>: PB { 
  var wrappedValue: T
  var _b: () -> Void


  func p() { wrappedValue.p() }
  func b() { _b() }
}

extension BWrapper: PA where T: PA {
  func a() { wrappedValue.a() }
}

Making these types still requires overloads for each protocol type, but the number of types is far smaller. In the solution you rejected, it would require N factorial types, where N is the number of protocols you use. With this solution, you just need N types and N extensions for each type, which is a lot more manageable. Once you have these types, you could dynamically create a type conforming to either A or B or A & B.

func loadDataFromServer() -> any P {
  let conformsToA = Bool.random()
  let conformsToB = Bool.random()
  let data: any P = URLSession.shared.grabAwesomeDataConformingToP()

  if conformsToA && conformsToB {
    return AWrapper(wrappedValue: BWrapper(wrappedValue: data) { /* code here */ } ) { /* code here */ }
  } else if conformsToA {
    return AWrapper(wrappedValue: data) { /* code here */ }
  } else if conformsToB {
    return BWrapper(wrappedValue: data) { /* code here */ }
  } else {
    return data
  }
}
2 Likes

You could add some functions to make the process easier and type-erase the results while still keeping the generic conformances.

func addAConformance(to data: any P, conformance: @escaping () -> ()) -> any P {
    _addAConformance(to: data, conformance: conformance)
}

private func _addAConformance(to data: some P, conformance: @escaping () -> ()) -> some P {
    AWrapper(wrappedValue: data, _a: conformance)
}

func addBConformance(to data: any P, conformance: @escaping () -> ()) -> any P {
    _addBConformance(to: data, conformance: conformance)
}

private func _addBConformance(to data: some P, conformance: @escaping () -> ()) -> some P {
    BWrapper(wrappedValue: data, _a: conformance)
}

And then you could rewrite the original function like this.

func loadDataFromServer() -> any P {
  let conformsToA = Bool.random()
  let conformsToB = Bool.random()
  var data: any P = URLSession.shared.grabAwesomeDataConformingToP()

  if conformsToA {
    data = addAConformance(to: data) { /* code here */ }
  }

  if conformsToB {
    data = addBConformance(to: data) { /* code here */ }
  }
}