Suppose Swift had the concept of associatedprotocol
for protocols, that is, declaring that conforming types must specify a protocolalias
that's going to be used in the type definition.
Thus, something like this:
protocol Transport {
associatedprotocol ActionType: Action // here `:` means "extending"
func send(action: any ActionType)
}
class BluetoothTransport: Transport {
protocolalias ActionType = BluetoothAction
func send(action: any BluetoothAction) {}
}
So, if I have a BluetoothTransport
instance, I will only be able to send
an instance of any BluetoothAction
and it would still conform to the protocol.
Personally, my next question would be: what's the point of the Transport
protocol at all? What's its usage?
You mention something like storing a BluetoothTransport
in a array of [any Transport]
, but to what end? If I get the first member
let anyTransportArray: [any Transport] = ...
let transport: any Transport = anyTransportArray[0]
what can I do with the transport
instance? The only visible interface is this:
func send(action: any Action) // it's `any Action` because its the most specific constraint for `ActionType` is that it extends `Action`
but this doesn't make any sense, because I would be able to send any Action
to an instance of any Transport
that certainly requires something more specialized (for example, if it was a any BluetoothTransport
, it would require any BluetoothAction
, not any Action
).
So, your example seems (again, if I understand it well) completely unsound, and something like that should not be permitted by the compiler, ever. The theoretical underpinning here is that a function with a more general argument as input is not a supertype of a function with a more specific argument as input.
But the following example would instead make sense:
protocol TransportReceiver {
associatedprotocol ActionType: Action
func receive() -> any ActionType
}
struct BluetoothTransportReceiver: TransportReceiver {
protocolalias ActionType = BluetoothAction
func receive() -> any BluetoothAction
}
Now if I have an any TransportReceiver
(that's internally a any BluetoothTransportReceiver
), the receive function would yield a any Action
instance, which is a supertype of any BluetoothAction
, so no problem here. That's because a function with a more general argument as output is a supertype of a function with a more specific argument as output.
So, I'm not saying that something like associatedprotocol
is wrong per se: I'm saying that your particular example is simply unsound, and I would suggest changing the design completely.
That's also because it doesn't seem particularly useful. You mentioned that you're doing something like @anon9791410 suggestion, that is, using classes for defining the Action
hierarchy:
class Action {}
class WifiAction: Action {}
class BluetoothAction: Action {}
class WriteWifiAction: WifiAction {}
class WriteBluetoothAction: BluetoothAction {}
protocol Transport {
associatedtype SubAction: Action
func send(action: SubAction)
}
class BluetoothTransport: Transport {
func send(action: BluetoothAction) {}
}
BluetoothTransport().send(action: WriteBluetoothAction()) // âś…
But this doesn't make much sense, for the same reasons I mentioned before: if you have a container of any Transport
, Swift will (correctly) not allow you to use the send
function:
let xs: [any Transport] = [BluetoothTransport()]
xs[0].send(action: WriteBluetoothAction()) // error: Member 'send' cannot be used on value of type 'any Transport'...
To fix the error, you need to cast it:
(xs[0] as! BluetoothTransport).send(action: WriteBluetoothAction()) // compiles just fine
If the example was instead similar to the one I wrote, based on a protocol with a function that yields a Action
this would work just fine:
protocol TransportReceiver {
associatedtype SubAction: Action
func receive() -> SubAction
}
class BluetoothTransportReceiver: TransportReceiver {
func receive() -> BluetoothAction { .init() }
}
let xs: [any TransportReceiver] = [BluetoothTransportReceiver()]
xs[0].receive() // compiles just fine
This has nothing to do with syntax of protocols, of the lack of something like associatedprotocol
.
So, I'm going back to my previous question: what's the point of the Transport
protocol? A protocol is useful when you can use it either as constraint in a generic context, or as existential box with "generic producer" semantics (that is, functions that return something generic, instead of taking something generic as input). In your specific case, you might as well forgo completely the protocol and write something like
class BluetoothTransport {
func send(action: any BluetoothAction) {}
}
without any conformance. When you need to store it in a array, you can simply store it in [Any]
, because when extracting a value you would still need to cast it to provide the compiler with sufficient context in order for it to decide if the code makes sense or not. If you want to have some sense of "constraint" you could come up with some kind of empty "marker" protocol, just to be able to write something more specific than [Any]
. For example:
protocol TransportMarker {}
protocol Transport: TransportMarker {
associatedtype SubAction: Action
func send(action: SubAction)
}
let anyTransportArray: [any TransportMarker] = ... // a tiny little better than `[Any]`
Here, instead, it seems that you're declaring a protocol just for having types conform to it, without any usage in a generic context.