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

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.

14 Likes

Is associatedprotocol really no-go? Does it make a sense like this (rewritten OPs example)?

protocol Action {

    associatedtype Result
}

protocol Transport {

    associatedprotocol A: Action

    func send<T>(_ action: T) async throws -> T.Result where T: A
}

// IMPL

protocol BluetoothAction: Action {}

final class BluetoothTransport: Transport {

    // Fulfills the requirement, with `A` being `BluetoothAction`
    func send<T>(_ action: T) async throws -> T.Result where T: BluetoothAction {
        ...
    }
}

// Utilizing the abstraction

actor TransportManager {

    func send<A, T>(_ action: A, on transport: T) async where T: Transport, A: T.A {
        do {
            let result = try await transport.send(action)
            // ...
        } catch {
            // ...
        }
    }
}

It’s certainly possible. The formal model I described in Slides from a talk about Swift generics would need some major revisions to handle conformance requirements where the right hand side is another type parameter, and we’d need to solve questions of soundness and decidability, etc.