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

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

The non-compiling code might look like that now, but if you see it that way, you're not seeing through to the intent.

There's probably some existing language precedent out there so nobody would need to imagine what we're talking about, but I don't know it.

1 Like

Here's an example that seems like it should be sound but is illegal in current Swift:

protocol MyDelegateProtocol: AnyObject {
}

class MyDelegate<ViewType: UIView>: MyDelegateProtocol {
    func getView() -> ViewType { ... }
}

func getViewIfPossible(delegate: any MyDelegateProtocol) -> UIView? {
    if let delegate = delegate as? MyDelegate /* I don't care what specific view type it is */ {
        return delegate.getView()
    }
    return nil
}

I remember back in Swift 5.4 I wanted something like this but I couldn't write it. Maybe it's possible with the associatedtype improvements in Swift 5.7, but it was a frustrating limitation at the time.

(I don't remember the context off the top of my head, but I know it was something like this where the exact generic type didn't matter since it was being casted to a known upper bound.)

1 Like

No, it’s not. What you’re asking for would require higher-kinder types. The type of delegate would be <T> where T == MyDelegate, T.ViewType: UIView. Swift does not allow the type of an expression to have any free variables.

1 Like

What is ...? If that works, the rest works. But it's better as

func getViewIfPossible(delegate: some MyDelegateProtocol) -> (some UIView)? {
  (delegate as? MyDelegate)?.getView()
}

Regardless, you can't say it like that. MyDelegate is a generic type, so you can't cast to it without specifying the type argument. You could say as? MyDelegate<UILabel>, for example, but to enumerate all possible subclasses is... impossible, to say the least.

It's something like that, but not that. If you don't specify, the class constraint is assumed. (UIView in this case.)

class MyViewDelegate: MyDelegate<UIView> {
  override func getView() -> UIView { .init() }
}

class MyTableViewDelegate: MyDelegate<UITableView> {
  override func getView() -> UITableView { .init() }
}

getViewIfPossible(delegate: MyViewDelegate()) // UIView?.some
getViewIfPossible(delegate: MyTableViewDelegate()) // UIView?.none

So, just switch to a protocol:

protocol Delegate<View>: MyDelegateProtocol {
  associatedtype View: UIView
  func getView() -> View
}

extension MyDelegate: Delegate { }

func getViewIfPossible(delegate: some MyDelegateProtocol) -> (some UIView)? {
  (delegate as? any Delegate)?.getView()
}

getViewIfPossible(delegate: MyViewDelegate()) // UIView?.some
getViewIfPossible(delegate: MyTableViewDelegate()) // UIView?.some

The protocol should be superfluous, but it's not. any MyDelegate should work, and would be a step towards what this thread is about. Until that kind of thing gets through Swift Evolution, we'll need protocols that only apply to one type, as a workaround. Simplified:

protocol P<T> { associatedtype T }
final class C<T>: P { }
struct S<T>: P { }

func ƒ<T>() -> some P<T> { S() } // compiles
func ƒ<T>() -> some C<T> { C() } // compiles except for the bug that says it's a redeclaration (https://github.com/apple/swift/issues/53122)
func ƒ<T>() -> some S<T> { S() } // An 'opaque' type must specify only 'Any', 'AnyObject', protocols, and/or a base class

Somebody made a thread about that recently but I can't find it.

1 Like

In order for this to be useful, you would also have to assume covariance for type parameters, which is exactly the wrong direction for certain types.

// Covariance works here:
class MyDelegate<T: UIView> {
    var view: T
}
var x: MyDelegate /* <UIView> */
x = MyDelegate(view: UITableView()) // ok, UITableView is subtype of UIView
x = MyDelegate(view: UILabel()) // ok, UILabel is subtype of UIView

// But it blows up here:
struct Function<Arg: UIView> {
  var impl: (Arg) -> Int
  func callAsFunction(_ arg: Arg) -> Int { impl(arg) }
}
var x: Function /* <UIView> */
x = Function { (UITableView) -> Int in
    return $0.numberOfSections
}
x(UILabel()) // BOOM! type system allows this because UILabel is subtype of UIView
2 Likes

I believe I see to the intent quite clearly, but what I'm trying to get at is that when there is a conflict between the rules of the language and some code, you cannot relax or bend the rules to accommodate that code without also accepting the consequences.

You don't need to go far to see this, and its consequences — Objective-C is a language with a thin veneer of a type system that will happily allow you to implement this; it conveniently ignores the Liskov substitution principle pretty much altogether.

You can represent some of the above Swift code with these Obj-C interfaces:

@protocol Action<NSObject> @end

@interface WifiAction: NSObject<Action>
- (void)broadcastOverWifi;
@end

@interface BluetoothAction: NSObject<Action>
- (void)broadcastOverBluetooth;
@end

@protocol Transport<NSObject>
- (void)send:(id<Action>)action;
@end

@interface WifiTransport: NSObject<Transport>
// Sorry, we only support sending Wifi actions.
- (void)send:(WifiAction *)wifiAction;
@end
Implementations
@implementation WifiAction
- (void)broadcastOverWifi {
    NSLog(@"%@", NSStringFromSelector(_cmd));
}
@end

@implementation BluetoothAction
- (void)broadcastOverBluetooth {
    NSLog(@"%@", NSStringFromSelector(_cmd));
}
@end

@implementation WifiTransport
- (void)send:(WifiAction *)wifiAction {
    [wifiAction broadcastOverWifi];
}
@end

The above code will compile without warning, and you can easily represent the concepts desired elsewhere in this thread (e.g., NSArray<id<Action>> for an array of any Action, NSArray<id<Transport>> for an array of any Transport, etc.).

The following will happily compile and run:

id<Action> action = [WifiAction new];
id<Transport> transport = [WifiTransport new];
[transport send:action]; // => broadcastOverWifi

However, the following code will also happily compile and run without warning:

id<Action> action = [BluetoothAction new]; // oops
id<Transport> transport = [WifiTransport new];
[transport send:action]; // 💥
2023-03-02 09:21:00.156 Untitled 4[63954:4630441] -[BluetoothAction broadcastOverWifi]: unrecognized selector sent to instance 0x600000978040
2023-03-02 09:21:00.157 Untitled 4[63954:4630441] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[BluetoothAction broadcastOverWifi]: unrecognized selector sent to instance 0x600000978040'
*** First throw call stack:
(
	0   CoreFoundation                      0x00000001991303e8 __exceptionPreprocess + 176
	1   libobjc.A.dylib                     0x0000000198c7aea8 objc_exception_throw + 60
	2   CoreFoundation                      0x00000001991d2c0c -[NSObject(NSObject) __retain_OA] + 0
	3   CoreFoundation                      0x0000000199096660 ___forwarding___ + 1600
	4   CoreFoundation                      0x0000000199095f60 _CF_forwarding_prep_0 + 96
	5   Untitled 4                          0x0000000104307b98 -[WifiTransport send:] + 64
	6   Untitled 4                          0x0000000104307c0c main + 92
	7   dyld                                0x0000000198cabe50 start + 2544
)
libc++abi: terminating with uncaught exception of type NSException

The assertions being made here by the types are simply violated: clearly, any Transporter cannot arbitrarily receive any Action.

Objective-C will happily allow these violations to compile, leaving you to figure out at runtime what went wrong, possibly arbitrarily far away in time and space. Swift makes the choice to disallow this from being written in the first place.

What's important to note is that in this specific case, it's not the rules here that are being bent or broken: the implementation is broken. Objective-C looks the other way when the code claims "sure, this thing that accepts only squares will happily take any other shape", while Swift says "yeah, I don't think so". This is what makes the specific discussion here more involved than "can we lift this restriction from the type sytem?": the request itself is inconsistent.


What's absent from this thread is any sort of consistent answer to what do you want to have happen when someone hands you a BluetoothAction when you only accept WifiAction? This is the part that can't just be hand-waved away with a "I don't know, just make it work", because you have to come to some sort of decision.

Objective-C says "I'll let you figure it out at runtime, possibly by crashing, or invoking unexpected behavior", which is one valid answer. Swift could choose to say "I'll deterministically crash", or "I'll continue executing arbitrarily, bitwise-casting the types to make the square peg fit into the round hole at any cost" — those are both valid answers. But they're both answers that go against the guarantees that Swift tries to make everywhere else.

So the question is: we can make this work, but what exact behavior do you want to have happen when arbitrary type A is passed to a function that takes an unrelated type B? You cannot simply make this work without answering this question.


Objective-C Aside

I'll add that the discussion here isn't just abstract — the fact that Objective-C allows this sort of operation to happen willy-nilly has resulted not just in buggy code, but in some pretty egregious security violations and breaches. It turns out, it can be pretty easy for an attacker to manipulate behavior to cause unrelated objects to get passed into methods, and when both the compiler and the runtime play along willingly, you can get into all sorts of trouble. Swift tries to avoid this by not playing along at all.

13 Likes

Yeah this is feasible now, makes sense. Like I said earlier I originally ran into this in Swift 5.4, so if I made it a protocol it became far less useful in other ways. Thanks for confirming.

Oh, I don't think it's useful*. But that's how it actually works—it's not hypothetical.

class C { }
struct S<T: C> { }
S() // S<C>

Similarly to how we don't have what this thread is about—an enforcement meaning "only a protocol that derives from another protocol"—we don't have a constraint that enforces only subclasses, rather than "either the superclass, or any subclass". Given that lacking state of affairs, I guess it makes some kind of sense. *Do people make use of it?

That's not how things work.

I understand that some of you think this is what we're talking about. It's not. When I find what we're talking about, in practice, if I remember, I will come back here and update with a link.

1 Like

Can you please explain what you mean, where my example has anything to do with the behaviour sub typing specified by Liskov.

First, it's quite a complex topic that even Liskov herself found difficult to define.

Second, behavioural sub-typing is something that you cannot decide for all cases, it's unsolvable.

Thirdly, from WikiPedia, this statement is something that always destroys any attempt to use this principle in real world applications on modern languages :

Also, more subtly, in the context of object-oriented imperative programming it is difficult to define precisely what it means to universally or existentially quantify over objects of a given type, or to substitute one object for another. When applying subtyping, generally we are not substituting subtype objects for supertype objects, we are simply using subtype objects as supertype objects. That is, it is the same objects, the subtype objects, that are also supertype objects.

What I am talking about is this:

The syntax for Generics/Inheritance and Protocols for Swift is a mishmash of different systems that confuses and convolutes the entire language, by making it so complex, that you cannot clearly see the best practice for doing anything with the system.

I know they say that with great power, comes great responsibility, but the breadth of possible paths to a solution using any/either of some, any, <T:> where, etc... has gone beyond the standard level of what I believe would be reasonable for anyone to comprehend.

The language is becoming a barrier to find a simple solution.

Sometimes I wonder if I should just switch to C. ;)

It seems to me that, in this thread, you got a lot of qualified answers for many specific concrete cases, from myself included, and several solutions were offered. The language is complex because things are complex: it often happens that things seem like they should work, while they don't, but upon further investigation it actually makes sense that they don't.

Note that I'm not suggesting that the Swift type system is complete: I think it's far from it, and in this thread some holes where clearly shown, but not really from the code you presented. In fact, the fact that some of your examples shows functions that took any parameters as input tells me that something is missing from your understanding of what's the point of an existential, which in Swift, precisely for added clarity, is explicitly spelled out with any.

The general rant

doesn't make much sense to me: some things are essentially complex, and at least Swift language design tries to smooth things out by

  • adopting a syntax that uses, where possible, plain english words, and readable constructs;
  • allowing for progressive discovery and incremental adoption of more complex features.

Now, don't get me wrong: I think we could greatly improve the way things are explained in the Swift language guide. For example, I think it would be very useful to explicitly distinguish between a type (like a class, or an existential) and a type class (like a protocol), and in general, the initial treatment, especially in the early stages of Swift, of protocols like "types", that "can be used like any other type", has been in retrospect, to me, detrimental to the general understanding of the language type system, especially when more powerful features were gradually adopted.

In the meanwhile, though, threads like these can be useful to untie the nasty knots.

Rather than C, a better fit could be any dynamically typed language, where you just write what you think should work, and then at runtime you discover if that's the case: what often happens is that it works in the limited subset of cases that you test it out with (after some trial and error), but then, in the future, new cases are introduced, and they break at runtime. And that's why all these dynamically typed languages have gradually introduced a form of static typing via annotations or typed supersets.

7 Likes

Sure. In both the PA/PB/PC and Action/Transport examples, you specify a goal of defining types S and T such that S <: T, where T has a method taking a parameter Pₜ, and S specializes that method to take a parameter Pₛ where Pₛ <: Pₜ. A concrete realization of this:

protocol Pₜ {}
protocol Pₛ: Pₜ {}

class T {
    func ƒ(_: Pₜ) {}
}

class S: T {
    override func ƒ(_: Pₛ) {}
}

Now, the definition of subtyping is that S <: T implies that a value of type S should be able to be used wherever one can use a value of type T (we don't even have to get into behavioral subtyping here, just abstract type relationships and subsumption) — i.e., if there is a set of properties (not in the Swift sense, but abstractly) that define a type T, by definition, those properties should also apply to type S. (Type S is allowed to add more properties to its definition, but not remove any.)

  • Here, the type T has one property: it has a function ƒ whose domain is all types conforming to Pₜ (i.e., if I have an instance of T, I am allowed to pass in any value which conforms to Pₜ).
  • The type S attempts to refine that property, by narrowing the domain of ƒ to only accept values conforming to Pₛ

If the subtyping relationship is valid, I can pass a value of type S anywhere that expects a value of type T... except, if I do so, I might violate S.ƒ's domain:

// No relationship to `Pₛ` whatsoever.
struct X: Pₜ {}

// Since `S` inherits from `T`, I can safely substitute it.
let t: T = S()

// Since `T` accepts _any_ `Pₜ`, this is valid
t.ƒ(X()) // 💥 `S.ƒ` _only_ accepts `Pₛ`es!

We have arrived at a contradiction: we cannot have all three of

  1. Refining of properties in subtypes to narrow their definitions
  2. Substitutability of subtypes for supertypes
  3. Consistency of properties

In other words, we have to at least:

  1. Ban subtypes from overriding supertype properties with narrower definitions (so we can continue to substitute subtypes for supertypes freely, without breaking their contracts)
    class S: T { override func ƒ(_: Pₛ) {} // ❌ Invalid
    let t: T = S() // ✅ Valid
    t.ƒ(X()) // ✅ Valid
    
  2. Ban substituting subtypes for supertypes (so we can continue to refine properties in subtypes while maintaining their contracts)
    class S: T { override func ƒ(_: Pₛ) {} // ✅ Valid
    let t: T = S() // ❌ Invalid
    t.ƒ(X()) // ✅ Prevented, because `t` can only have type `S`, and `S.ƒ`
             // won't accept `X`
    
    t.ƒ(SomePₛ()) // ✅ Valid
    
  3. Allow breaking type contracts (so we can continue to refine properties in subtypes while still allowing substituting subtypes for supertypes freely)
    class S: T { override func ƒ(_: Pₛ) {} // ✅ Valid
    let t: T = S() //  ✅ Valid
    t.ƒ(X()) // 💥 Allowed
    

The Liskov substitution principle primarily just says that you should choose (1).

You also don't have to. Objective-C, for example, chooses (3): as a highly dynamic language, it relegates the behavior to whatever might happen at runtime ("who knows, maybe it'll work out?"). Many other dynamic languages do the same. However, it does happen to be that "whatever might happen at runtime" is inherently unpredictable, and largely unsafe (quite possibly catastrophically so — and I'm not talking about crashing, but egregious and numerous security violations and attack vectors).

Swift, at its inception, chose (1): for type-system consistency, you simply cannot refine supertype properties by narrowing (it actually goes a step further, by not allowing you to refine many properties at all, even if otherwise valid by LSP rules). Changing this requires a re-evaluation of the type system, and critically, requires deciding what happens when you break the rules.


The syntax for Generics/Inheritance and Protocols for Swift is a mishmash of different systems that confuses and convolutes the entire language, by making it so complex, that you cannot clearly see the best practice for doing anything with the system.

I know they say that with great power, comes great responsibility, but the breadth of possible paths to a solution using any/either of some, any, <T:> where, etc... has gone beyond the standard level of what I believe would be reasonable for anyone to comprehend.

I bring all of this up to try to express that this isn't something to be taken lightly. Swift does have a lot of complexity, yes, because it solves a lot of complex problems — but you can't ask it to "get out of my way" without also introducing some much bigger concerns. It's unfortunate that there are some programs that aren't easily expressible in Swift today (and some that are theoretically unexpressible, which is true of every consistent language), but a fundamental shift like this in the type system requires some pretty Herculean justification.

Swift tries to improve on languages from the past and from mistakes learned, and it's not by coincidence that the type system is designed the way that it is.

12 Likes