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

this breaks down when you want the associatedtype itself to be a generic type. i have often wanted to do something like

protocol Transport<Technology>
{
     associatedtypeconstructor Technology<some OutputBuffer>

    func send(action:Technology<some OutputBuffer>)
}

extension BluetoothTransport:Transport
{
    func send(action:WriteBluetoothAction<some OutputBuffer>)
}
extension WifiTransport:Transport
{
    func send(action:WriteWifiAction<some OutputBuffer>)
}

in the past i have found that many limitations in the type system can be worked around by just adding more type parameters. but this is one limitation i have not yet discovered a workaround for. the best we can do is define a protocol for each possible associated type constructor, which gets really hairy really fast.

2 Likes

This seems a cousin of the lack of higher-kinded types. If we had something like an associatedtypeconstructor, a Functor protocol could be defined like this

protocol Functor {
  associatedtype Wrapped
  associatedtypeconstructor Witness<Wrapped> = Self

  func map<Other>(transform: (Wrapped) -> Other) -> Witness<Other>
}

extension Optional: Functor {
  func map<Other>(transform: (Wrapped) -> Other) -> Optional<Other> {
    ...
  }
}
1 Like

I actually did something similar to this, by providing an independent identifier, instead of using conformance directly.

It's not what I had hope for, but it provides a good way to check this in runtime.

To follow this up, I've actually removed the problem altogether by making the abstraction at one level higher.

What that means, is that Transport is now wrapped in Service. The transport hides all implementation details of how it handles an action. Commands don't exist across transports, all those details are hidden in the Transport.

Inside the Service, an Action is no longer bound to any specific Transport. A service can have any kind of transport, each type simply accepts an action, and performs what it needs to do while hiding the implementation details.

It's immensely easier to understand and use now. As they say KISS

An example of usage is this.

protocol Action {}
struct DoSomethingAction: Action {} // Actions are transport agnostic

class Service {
   let transport: some Transport
   func perform(action: any Action) {
      transport.perform(action: action)
   }
}

protocol Transport {
  func perform(action: any Action)
}

class BluetoothTransport: Transport {
  func perform(action: any Action) { 
  //... All implementation details are now hidden
  }
}

class WifiTransport: Transport {
  func perform(action: any Action) { 
  //... All implementation details are now hidden
  }
}

Doesn't work. What does that really look like?

Why can you not replace the anys with somes?

That's not an override. It's an overload.

Here's how you'd do it with classes.

class Transport<Action: ModuleName.Action> {
  func send(action: Action) { }
}

class BluetoothTransport: Transport<BluetoothAction> { }

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

class 🪄: BluetoothAction { }
BluetoothTransport().send(action: 🪄())
1 Like

Oh true, there’s no override keyword, either. (Boy, I’m thankful it exists! Though I didn’t notice it’s absence)

I deleted my post so I don’t mix up anyone else

Maybe you can, I haven't tried.

Oh, to make matters worse, using "any Class" FORCES you to break this principle if you use function calls without generics.

You cannot access static protocol specific implementations in the following :wink:

func performAction(action: any Action) {
  // No way to access static vars/function of `action`
}

Only the following works :

func performAction<T: Action>(action: T) {
  // What's the point of using any then?
  T.staticVar
}

Instance members. As much as the system is lacking in terms of protocol derivation, static existentials can't make sense, as Xiaodi Wu tried to get across above. (This answer is much more clear to me:)

1 Like

From reading the comments I see two things to note:

  1. there are lots of comments about existential types not being able to conform to protocols for lack of required members. But, the object in the box which must conform, does have such members. So, the existential type can delegate to the contained instance. Problem solved.

  2. conformance vs inheritance came up several times. While I understand the difference I am not sure that it is useful for the consumer of swift. It is useful to the code generator in the compiler, but it may be making swift harder to learn and use.

As noted previously, the Self type makes this impossible to make sound in all cases.

If it were possible to ā€œjustā€ make existentials conform to protocols by delegating through the box, it would have been done ages ago.

4 Likes

At the end of the day, it really just shouldn't be this difficult to create object abstractions.
Could be as a result of legacy support for classes, or the obfuscation by using the same syntax for either class/protocol inheritence/conformance.

However, as a consumer of the language, it feels all a little broken overall.

As mentioned, it just shouldn't be this difficult to do something so simple. The language should take some responsibility for creating a syntax that only makes a few possible ways of doing this available, otherwise we get into what I call "semantic syntax soup". Where you can no longer derive meaning from the syntax.

1 Like

It depends on the members. The existential box will erase some type information, and the compiler might not be able to infer what an instance is capable of in a certain context if it's an existential box. This discussion pretty much shows that things are more complex than it seems at first glance.

The compiler could dynamically dispatch the method call to the boxed instance, but in the context where it's used it might not be able to understand the actual result of the dispatch (for example, when dealing with associated types).

No, it's "useful" to the user too: as I mentioned, there's a difference between a "type" and a "type class", and if you're using the type system to enforce invariants, you must understand and leverage that difference. The alternative is simply to not leverage the type system, or to only use part of it, and write something actually simpler (like what @realityworks ended up doing, by removing the relationship between Action and Transport).

These discussions about protocols, conformances and existentials can be broadly split in 2 groups:

  • some are genuine showings of missing features of the Swift type system, considering the underlying type theory;
  • others look like that, but instead what's being asked is actually unsound and should be prevented by the type system.

Also, there's often a lack of understanding of what's an existential box for: in general, for example, it doesn't make much sense to accept any Something as input to a function. The prime use cases, AFAICT, of existential boxes are:

  • abstract properties;
  • heterogeneous collections.

What you were trying to do, though, was neither simple nor actually sound. Your final solution, that removes the relationship between Action and Transport is actually simple instead, and the Swift compiler gladly accepts it.

3 Likes

If you want this design, you can write your own type-erasers. I see some confusion about any. It's not a constraint - it's a box. The keyword saves you the trouble of writing the box, with the caveat that any Protocol does not conform to Protocol - which is solved by allowing programmers to extend the box.

protocol Action { }
protocol ActionA: Action { }
protocol ActionB: Action { }

struct WriteA: ActionA { }
struct AnyActionA: ActionA { init(_ action: some ActionA) { } }

struct WriteB: ActionB { }
struct AnyActionB: ActionB { init(_ action: some ActionB) { } }

protocol Transport {
    associatedtype T: Action
    func send(action: T)
}

class TransportA: Transport { func send(action: AnyActionA) { } }
class TransportB: Transport { func send(action: AnyActionB) { } }

let transportA = TransportA()
transportA.send(action: AnyActionA(WriteA())) // āœ…
transportA.send(action: AnyActionB(WriteB())) // āŒ

let transportB = TransportB()
transportB.send(action: AnyActionA(WriteA())) // āŒ
transportB.send(action: AnyActionB(WriteB())) // āœ…

To be very clear, this is not some implementation deficiency of Swift to be "solved." Saying that all existential types do not conform to their protocols is like saying that all rectangles are not squares. Let's work through a simple example:

Consider the existing standard library type CollectionOfOne<T>: it is a collection that holds exactly one element of type T.

Suppose I create two additional types, PairOf<T> and TripleOf<T>, which hold exactly two or three elements of type T, respectively. Together, these three collection types each have a fixed size (1, 2, or 3).

I can create a common protocol, CollectionOfFixedSize. For the purpose of this example, ignore all syntactic requirements; consider simply the semantic requirement of this protocol that a type which conforms to CollectionOfFixedSize has to be, well, a collection of a fixed size. Put another way, for any given conforming type, any value of that type has the same number of elements as any other value of the same type.

Each of the three collection types above meet this requirement and can correctly conform to this protocol.

Now, consider the existential type any CollectionOfFixedSize. By construction, it can represent any value of any collection of fixed size—if it could not, it would not be an existential type.

Therefore, you must be able to write:

var x: any CollectionOfFixedSize = PairOf<Int>(42) // x.size == 2
x = TripleOf<Int>(42)                              // x.size == 3

As you can see, x is not a collection of fixed size. This demonstrates that any CollectionOfFixedSize does not fulfill the semantic requirement of CollectionOfFixedSize.

Whether the compiler can or can’t synthesize any syntactic requirement is immaterial: it is (and must be) unsound to consider the type any CollectionOfFixedSize to be a collection of fixed size.

Likewise, if I created a protocol called HasTwoPossibleValues, I could correctly conform Bool and any enum with two cases to that protocol, but any HasTwoPossibleValues can represent more than two possible values.

And, as mentioned before, any FixedWidthInteger is not a fixed-width integer type.

Not every any P conforms to the corresponding protocol P.

31 Likes

i absolutely do see this as an implementation deficiency of swift to be "solved". sometimes you really do want to use the concept of a regular polygon, instead of getting lost in rectangles versus squares.

i can’t count how many times i have wanted to do something like:

protocol ShapeDelegate
{
    associatedtype Sides:CollectionOfFixedSize<some BinaryFloatingPoint>

    func perimeter(sides:Sides<some BinaryFloatingPoint>)
}

but we cannot do this in swift. so we have to resort to

protocol TriangleDelegate
{
    func perimeter(sides:TripleOf<some BinaryFloatingPoint>)
}
protocol RectangleDelegate
{
    func perimeter(sides:QuadrupleOf<some BinaryFloatingPoint>)
}
protocol PentagonDelegate
{
    func perimeter(sides:QuintupleOf<some BinaryFloatingPoint>)
}

a generic type cannot witness an associated type requirement, only a specialization of a generic type can do that. and this is why people continually complain why existentials cannot do ____, because the language has provided no alternatives to existentials.

2 Likes

To be completely honest, I have trouble following these kinds of discussions, for several reasons.

First of all, it’s not clear to me what actual thing this specific ShapeDelegate example is supposed to represent in practice, or what static invariants we are trying to guarantee here. Maybe a more realistic example would help. Why do you need this abstraction and what would it actually enable in terms of safety, reuse or performance?

More broadly, to make effective use of static types I think it’s important to have a clear understanding both of the abstractions offered by the language and the problem domain being modeled.

I’m not sure if you’re looking for generic associated types, self-conforming existentials, or both. In conclusion, this entire thread leaves me rather confused.

5 Likes

here is a "realistic" example:

protocol BSONDecodable
{
    init(bson:BSON.AnyValue<some _Storage>) throws
}

this protocol is hard to provide an init(bson:) witness for, because it is complicated to go from BSON.AnyValue<some _Storage> to the thing you actually want to decode from, such as:

extension MyCodableThing
{
    init(bson:BSON.ListDecoder<some _Storage>) throws
}
extension MyOtherCodableThing
{
    init(bson:BSON.DocumentDecoder<some _Storage>) throws
}
extension YetAnotherCodableThing
{
    init(bson:BSON.UTF8<some _Storage>) throws
}

so i end up doing

protocol _BSONListDecodable:BSONDecodable
{
    init(bson:BSON.ListDecoder<some _Storage>) throws
}
protocol _BSONDocumentDecodable:BSONDecodable
{
    init(bson:BSON.DocumentDecoder<some _Storage>) throws
}
protocol _BSONStringDecodable:BSONDecodable
{
    init(bson:BSON.UTF8<some _Storage>) throws
}

but this is an awful API because specifying _BSONListDecodable, etc. as a generic constraint is almost always a mistake, but users are expected to conform codable types to _BSONListDecodable, etc. in order to avoid having to implement the ā€œrealā€ BSONDecodable protocol’s requirement.

i am looking for generic associated types, but many others in this thread are looking for self-conforming existentials. i think it is difficult to recommend what people should be doing besides wishing for self-conforming existentials, because in order to

we need tools in the language to express something like BSONDecodable with static types.

1 Like

What @xwu is getting at, and that I believe you understand (though disagree with), is that this issue isn't a technical limitation of Swift, but an explicit design decision to not support, because of the implications for the type system.

I'm guessing that you're aware of this, but any logical system has an inherent conflict between soundness and completeness. Soundness implies that truthful statements in the system can only be used to lead to more truthful statements, while completeness implies that all truthful statements can be produced by the system, one way or another. Less abstractly, for type systems, this means that:

  1. A sound type system is one which only allows programs with valid type relationships to compile
    • i.e., "if it compiled, it must have been valid"
  2. A complete type system is one which will compile any program with valid type relationships
    • i.e., "if it is valid, it will compile"

Soundness is great for correctness: "the program compiled, so from a type perspective, it will do what I want". Completeness is great for getting stuff done: "man, I'm glad the compiler got out of my way to let me do what I want".

Unfortunately, you cannot have both, not as an implementation limitation, but as a limitation of the universe. It is not possible to have a system which is both completely sound and complete at the same time: either the system is sound but there will be programs that exist that, although correct, will not compile, or the system is complete but there will be programs which are incorrect that do compile. In a very practical sense, it comes down to tradeoffs, and priorities.

Static type systems tend to lean more toward providing soundness, while dynamic type systems tend to lean more toward providing completeness. Swift, as a matter of intentional design, leans heavily toward soundness, even at the cost of certain programs being impossible to represent.


The examples that both you and @realityworks have provided are pretty clear violations of the Liskov substitution principle, to which Swift adheres. This principle covers some pretty fundamental rules that prop up the soundness of subtyping relationships, and breaking these rules leads to inconsistencies in the type system that cannot somehow be patched up.

Whether intentionally or not, the request here ends up boiling down to Swift relaxing its rules about subtyping relationships — which I think is antithetical to Swift's goals. This also isn't something that can be supported "just a little bit", for example, but not more generally.

(This isn't at all to say that Swift's type system is perfect, or that there aren't countless cases where the compiler can do more to allow correct-but-hard-to-express programs to compile more easily, but this is a pretty foundational set of rules to relax that I simply don't see happening.)


In very reduced terms: it’s not possible to have 1 + 1 occasionally equal 3 without also having some pretty far-reaching consequences.

17 Likes