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?