Typealias conformance to protocols, why can't this simple thing be implemented?

Ok, so this may have been asked many times before, but I have no idea still, to this day, why this very simple thing cannot be implemented.

protocol PA { }
protocol PC: PA { }

protocol PB {
    associatedtype TypeA: PA
    func aFunction(param: TypeA)
}

class SpecialisationOfPB: PB {
    typealias TypeA = PC
    func aFunction(param: any TypeA) // Or anything similar
}

The call to aFunction cannot be done and in the case where I may want to have more abstraction, it breaks further.

The problem this causes is I cannot create protocol conformance which specialise their types to another form of conformance.

This use case is extremely common and would be similar to this.

protocol Transport with protocol Command

Specialised to class BluetoothTransport with protocol BlueToothCommand

So the associated type is not a concrete type.

I don't want to read about WHY it doesn't work, I want to read about what's being done to make this work, if anything...

1 Like

It's not exactly clear to me what you're asking here specifically, but if you're wondering why an existential type doesn't automatically conform to its own protocol, it's because this is impossible in the general case depending on what the protocol requires:

Consider if a protocol P has a required static property foo without a default implementation, meaning that you must implement this static property foo on any type that conforms to P, and then users can get the value of this property for any conforming type without having any instances of that type: the existential type any P can't automatically pull an implementation of this static property out of thin air, so naturally it doesn't conform to P.

In the future, Swift could consider allowing you to manually declare conformance of any P to P in an extension that supplies manual implementations of any such requirements. There hasn't been anyone working on this direction to my knowledge. In the meantime you can achieve essentially the same manual implementation in principle by creating your own concrete type-erased type (by convention, for a protocol P, you'd call it AnyP; you'll see several types just like it in the standard library) and conforming that to the protocol manually.

Even then, there will always be protocols with semantic requirements that are impossible even manually to conform. For instance, in the example above, if P is Animal and the static requirement foo is species, it is self-evident that any Animal cannot implement such a requirement (an existential box for an animal without an underlying animal type has no species) and therefore the existential type does not conform to Animal. Nothing is being done to make this work because nothing can be done to make this work: it is not theoretically possible.


By the way, this line is an issue here actually, and either we've already locked in that it will be rejected in future language versions or we will need to.

In Swift 5 and earlier, PC is the spelling for two things, the protocol and the existential type. In modern versions of Swift, the existential type is now spelled any PC but we still allow the spelling PC for source compatibility (if the corresponding protocol has no Self or associated type requirements).

As the keyword suggests, typealias is for aliasing types; however, when we made the change above it was pointed out that users have good reason still to want to alias protocols too, so it'll continue to be allowed to do so going forward even though it's not actually a type alias.

However, in this example, your protocol PB has set down a requirement: it says, "give me a type which I will associate with the requirement TypeA."

In order to fulfill that requirement, typealias TypeA = PC in a conforming type would have to alias an actual type. If it does, it must be taking advantage of the legacy spelling of the existential type any PC; in that case, though, you can't mix-and-match by prefixing the type with yet another any in the following line, writing any TypeA—that must be rejected because it'd then be equivalent to any any PC, which isn't a thing.

If any TypeA is to be accepted, on the other hand, then TypeA must be the alias for solely the protocol PC and not the existential type, and that has to be rejected because it wouldn't then fulfill the requirement to provide a type named or aliased to TypeA.

6 Likes

I think there are 2 issues here, in the sense that there's one issue with this code, and in case it's solved, then the second issue would manifest.

First associatedtype and typealias mean "a type that's associated" and "an alias to a type". But a protocol in itself doesn't declare a type: it would be more correct to call it a type class (a lĂ  Haskell). For example, the declaration

protocol PC: PA { }

describes a protocol (a type class) called PC that inherits from (not conforms to), thus extends, PA.

In the protocol declaration

protocol PB {
    associatedtype TypeA: PA
    func aFunction(param: TypeA)
}

when writing

associatedtype TypeA: PA

you're saying "types conforming to the protocol PB must declare a associated type (not type class) that conforms to (not inherits from) PA. Notice the difference between inherits from and conforms to.

But this code

typealias TypeA = PC
func aFunction(param: any TypeA)

seems to say "TypeA means in this context the protocol (type class) PC, and aFunction takes a as parameter a type, which should be treated as an existential box (which is a type, not a type class) of TypeA, thus PC". But this is not how protocols and types work (or will ever work): any TypeA makes sense only if TypeA is a protocol, which is not the case here.

Given the above, an hypothetical way to change the code in order to make it work could be the following:

protocol PB {
    associatedtype TypeA: PA
    func aFunction(param: TypeA)
}

class SpecialisationOfPB: PB {
    typealias TypeA = any PC
    func aFunction(param: TypeA) {}
}

now TypeA is a type, becase an existential box is a type, and it's spelled any ProtocolName in modern Swift. Nice, but it still wouldn't work, because of this line:

associatedtype TypeA: PA

As already mentioned, this declaration means "the type called TypeA (which, in SpecialisationOfPB is any PC) must conform to the protocol PA.

But, as explained by @xwu , it's simply impossible (like, theoretically impossible, without generating nonsensical behavior) to have an existential box to conform to a protocol (including, of course, conforming to the protocol from which the box is formed, or a subprotocol) in the general case.

As mentioned, there are cases in which it could be possible in a sound way, for example if we provide a default implementation for protocol members. For example, in this case

protocol PA {
  func frobulate() -> String
}

extension PA {
  func frobulate() -> String {
    "yello"
  }
}

the requirement to implement a frobulate function would be satisfied by the default implementation, that is, any PA (and also any PC and other inheritors) would conform to PA.

But there are cases where this is not possible even theoretically, for example if the protocol has an associated type, and a method that returns a value of that type. We could write several extensions for it with several default implementations constrained to specific concrete types, but it wouldn't be very practical.

1 Like

Guys, I understand the meaning of type alias and why it's not a suitable solution, I understand the semantics don't make sense.

How does one then, create conformances of the following, where I can constrain one protocol implementation to a subset of it's associated/or similar types:

protocol Action {}
protocol WifiAction: Action {} 
protocol BluetoothAction: Action {}

struct WriteWifiAction: WifiAction {}

struct WriteBluetoothAction: BluetoothAction {}

protocol Transport {
    send(action: any Action) // I WANT TO SPECIALISE THIS
}

class BluetoothTransport: Transport {
    // HOW DOES ONE DEFINE THIS FUNCTION 
    // TO ONLY ACCEPT A SPECIALISATION OF ACIONT?
    send(action: any BluetoothAction) {} 
}

let transport = BluetoothTransport()
transport.send(action: WriteWifiAction()) // SHOULD FAIL COMPILATION

The feeling has always been, that I can do this with associated types, but it's not a viable solution, and generics won't work in this sense either.

Thoughts?

Is this something that could be an option for an Evolution proposal?

1 Like

Thank you!

Thank you

Again, I'm not sure I understand what it is you're trying to do. As you've written it, BluetoothTransport doesn't—shouldn't—mustn't conform to Transport. That's because any conforming type must be able to "send" any Action in an existential box, whereas you're specifically saying that BluetoothTransport must not allow this for any Action but only implement "sending" for any BluetoothAction (again, in an existential box).

I'm also not sure why you're stipulating that these methods must take existential boxes as arguments. Why do you need this indirection?


Edit: You've updated your example; I see what you're driving at, will reply at a later time with further thoughts.

2 Likes

BluetoothAction is both any and some Action right? So what I'm saying is that I want the BluetoothTransport to be abstract enough to be held along with other transports in say a collection of Array<any Transport>(), but if you want to send it an action, that action must conform to BluetoothAction.

The goal is to be able to constrain implementations/specialisations to subtypes of their protocol requirements, allowing us to funnel types and create conformances in such situations which can resolve in compile, not runtime.

1 Like

What would happen in this case?

var transports: [any Transport] = [BluetoothTransport()]
transports[0].send(action: WriteWifiAction())

i.e. when I am passing some inappropriate action to your BluetoothTransport component. Compiler won't catch that, should it crash or throw at runtime?

If so perhaps you can do just this:

class BluetoothTransport: Transport {
    func send(action: Action) {
        precondition(action is BluetoothAction)
        // ...
    }
1 Like

Swift does not provide a mechanism to achieve what you're asking about, aside from with classes. It's a real problem that the syntax for "placeholder type that implements a protocol" is the same as "complete constrained hierarchy of subclasses"—but classes started that problem in the 1980s by conflating the concepts of concrete types and protocol hierarchies. It suggests that maybe you can make these kinds of refinements on protocols, but, no.

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()) // âś…

// Cannot convert value of type 'WriteWifiAction' to expected argument type 'BluetoothAction'
BluetoothTransport().send(action: WriteWifiAction())

I think this is sad and I'd like to see it fixed. Related things that need fixing:

class C1 { }
// "Type 'C3' constrained to non-protocol, non-class type 'C2'" 🤨
func Ć’<C2: C1, C3: C2>(_: C3) { }
protocol P1 { }
func Ć’<P2: P1, P3: P2>(_: P3) { }
3 Likes

Yes, indeed. If only we could do this with value types :(

This is exactly what I'm doing at this point.

It falls apart for classes too, as soon as you need multiple inheritance. I think subclassing is immoral, but its existence is useful to demonstrate that your ideas are not insane or inane. As you can see above, people are generally not going to have any idea what you're talking about though, because the conflation that classes wrought hasn't broken apart fully in the zeitgeist.

1 Like

I somewhat feel that the syntax of protocols for swift has become highly specific to only a subset of use cases for conformance and specialisation.

I'm seeing a lot of things break as you begin to use protocol conformance in more general cases.

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.

7 Likes

If anyone has an example of a use case for associatedprotocol I would love to hear about it!

Sorry for the delay—it's been a very busy few days, but I got a few moments to sketch out how to achieve something like what it seems you're going after, and I'll explain presently.

This is not true. It's been very possible to achieve in Swift going back several versions, and now with primary associated types, the syntax is even more ergonomic and readable.


As you can see, there needs to be an actual type that you can refer to so that the type system can make the association between, for example, BluetoothAction and BluetoothTransport. So, to start with, we will create entities in the type system that can be referred to when you're talking about Bluetooth versus Wi-Fi: these don't need to have any functionality, so we'll make them enums with no cases so that users can't create instances of them:

protocol _Technology { /* No requirements. */ }
enum WiFi: _Technology { /* No cases. */ }
enum Bluetooth: _Technology { /* No cases. */ }

Now we can express how BluetoothAction and BluetoothTransport are associated. It's not actually required to use primary associated types here, but we'll put them in because it makes the syntax easier to read:

protocol Action<Technology> {
  associatedtype Technology: _Technology
}
// Notice that you don't really need to define
// a separate `BluetoothAction` protocol, for example.

struct WriteBluetoothAction: Action {
  typealias Technology = Bluetooth
}

struct WriteWiFiAction: Action {
  typealias Technology = WiFi
}

protocol Transport<Technology> {
  associatedtype Technology: _Technology
  func send(action: some Action<Technology>)
  // Notice how we use only generics here,
  // without any runtime existential boxes.
  //
  // Without using the primary associated type,
  // this requirement can be equivalently written:
  //
  //   func send<A: Action>(action: A)
  //     where A.Technology == Technology
}

final class BluetoothTransport: Transport {
  typealias Technology = Bluetooth
  func send(action: some Action<Bluetooth>) {
    print("Sending \(Technology.self) action")
  }
}

This design allows users to provide only instances of types that conform to Action<Bluetooth> as arguments to BluetoothTransport.send:

let transport = BluetoothTransport()

transport.send(action: WriteBluetoothAction())
// "Sending Bluetooth action"

transport.send(action: WriteWiFiAction())
// Compile-time error:
// instance method 'send(action:)' requires the types
// 'WriteWiFiAction.Technology' (aka 'WiFi') and 
// 'Bluetooth' be equivalent
2 Likes

Phantom types are how I would have done it too, but it's not the same thing.

this will run into problems in practice, because you cannot actually implement this requirement with something like:

  func send(action:WriteBluetoothAction)

a generic implementation like:

is often not possible, you most likely need more information about the details of WriteBluetoothAction than some Action<Bluetooth> can provide.

My understanding of the problem as stated was that this is explicitly desired—that is, that a Transport type is meant to be required to be able to “send” any Action value that uses the same technology, not just a specific one.