Dispatch on associated type

Hi Swift forum community,

I'm trying to call a different func based on a protocols associated type.

One way this can be achieved is to add the func to the protocol requirements. That works but I'm looking for a solution where the func is not part of the protocol.

I've tried different setups, and commented about each attempt. I listed all attempts below (making this post quite long). To try them out, download the playground from github.

Protocol based dispatch

// Public
protocol P {
    associatedtype A: P
    var a: Self.A { get }
    // This example works if f() is part of P.
    //
    // When f() is not part of P, swift won't add f() to
    // the protocol's lookup table, causing f() to be only
    // directly dispatched.
    //
    // f() might not make sense to be part of P. Is it
    // possible to implement the same behaviour without
    // adding f() to P?
    //
    // Comment out the next line to see an infinite
    // recursion warning on line 27.
    func f()
}

protocol Q: P {}

extension Never: P {
    var a: Never { fatalError() }
}

struct R: P {
    var a: some P { S() }
}

struct S: Q {
    var a: Never { fatalError() }
}

// Internal
extension P {
    func f() { a.f() }
}

extension Q {
    func f() { print("done") }
}

R().f()

Protocol based dispatch using specialised protocols to hide f()

// Public interface
protocol P {
    associatedtype A: P
    var a: Self.A { get }
}

protocol Q: P {}

extension Never: P {
    var a: Never { fatalError() }
}

struct R: P {
    // This solution has no f() reqirement on P
    // and works as long as P.A is known to conform to _P
    // Switch the below lines to fix the compilation error `Type 'some P' does not confirm to protocol '_P'` on line 36
//    var a: S { S() } // this works
    var a: some P { S() } // `some P` does not conform to _P
}

struct S: Q {
    var a: Never { fatalError() }
}

// Internal, keeping f() inside
protocol _P: P where A: _P {
    func f()
}

// Another problem with this approach is that
// every type conforming to P has to also be
// conformed to _P. This prevents f() to work
// on P conforming types created by consumers
// of this API.
extension Never: _P {}
extension R: _P {}

protocol _Q: Q, _P {}
extension S: _Q {}

extension _P {
    func f() { a.f() }
}

extension _Q {
    func f() { print("done") }
}

R().f()

Generic dispatch

// Public
protocol P {
    associatedtype A: P
    var a: Self.A { get }
}

protocol Q: P {}

extension Never: P {
    var a: Never { fatalError() }
}

struct R: P {
    var a: S { S() }
}

struct S: Q {
    var a: Never { fatalError() }
}

// Internal
struct PWrapper<T: P> {
    let p: T

    func f() where T.A == Never { print("done") }
    func f() { PWrapper<T.A>(p: p.a).f() }
}

// Generic dispatch has my preference: It gives more
// optimisation hints to the compiler, and can be
// more clear in intention (less noisy).
//
// However I can't get the compiler to dispatch based on
// an associated type. (I'm not sure if I tested every
// possible implementation).
//
// In this example, f() on S() will run as expected;
// while f() on R() will go into infinite recursion.

// S.A is found to be `Never`
// prints "done"
PWrapper(p: S()).f()

// R.A.A is not used as a dispatch criteria
// infinite recursion triggered from here
PWrapper(p: R()).f()

Type check using is on P.A

// Public
protocol P {
    associatedtype A: P
    var a: Self.A { get }
}

protocol Q: P {}

extension Never: P {
    var a: Never { fatalError() }
}

struct R: P {
    var a: S { S() }
}

struct S: Q {
    var a: Never { fatalError("S.a called") }
}

// Internal
struct PWrapper<T: P> {
    let p: T
    func f() {
        print("Run f() on \(type(of: p))")
        if p.a is Never { print("done") }
        else {
            print("Call f() on \(type(of: p.a))")
            PWrapper<T.A>(p: p.a).f()
        }
    }
}

// This example uses the `type check operator 'is'` to
// test with an explicit condition if P.A is Never.
// Alas, testing the type of S.A will retrieve its value,
// causing a fatalError().

PWrapper(p: R()).f()
// Run f() on R
// Call f() on S
// Run f() on S
// __lldb_expr_69/TypeCheckConditional.xcplaygroundpage:18: Fatal error: S.a called

Type check on P

// Public
protocol P {
    associatedtype A: P
    var a: Self.A { get }
}

protocol Q: P {}

extension Never: P {
    var a: Never { fatalError() }
}

struct R: P {
    var a: S { S() }
}

struct S: Q {
    var a: Never { fatalError("S.a called") }
}

// Internal
struct PWrapper<T: P> {
    let p: T
    func f() {
        print("Run f() on \(type(of: p))")
        if p is S { print("done") }
        else {
            print("Call f() on \(type(of: p.a))")
            PWrapper<T.A>(p: p.a).f()
        }
    }
}

// This implementation is getting really close.
// No infinite recursion. P does not require f().
// The downside is that any type that has `P.A == Never`
// will crash f() unless f() has a condition to prevent that.

PWrapper(p: R()).f()

My question is: Is there another way to dispatch to different funcs based on the associated type (P.A) without changing the protocol (P)?

If by “dispatch” you mean specifically dynamic dispatch, then no:

It’s not clear to me why your function can’t be a part of the protocol, though.

If by “dispatch” you mean specifically dynamic dispatch , then no:

Thanks for the link, I'll study this. I'm not necessarily looking for dynamic dispatch. A generic implementation is fine too.

It’s not clear to me why your function can’t be a part of the protocol, though.

In my case, the protocol P is public and func f() is meant to be internal. It is not so much that I can't, instead it is a concern of keeping the public interface clean.
The problem worsens if there are multiple internal funcs that want to make this type of call. One might suggest to add a generic func to the protocol:

protocol P {
    associatedtype A: P
    var a: Self.A { get }
    func _internal_helper<T, R>(_ a: T) -> R
}

That one func can be dispatch for all the internal functionality. However, I'm not super happy with this kind of pollution on a public protocol.

Another reason why the func can't be part of the protocol is when that protocol is defined in an external dependency. If you only have access to the compiled library, there is no way to add the extra requirement.

In that case I’d agree that creating an internal protocol refining P, requiring f() via that internal protocol, and then conforming the relevant types to your internal protocol would be the best way to go.

3 Likes

Is there a way to dynamically test for Never?

Using p.a is Never will get the value of p.a, which is Never, and will stop the program:

if p.a is Never { print("done") }

Same with (but more obviously so) for type(of: p.a) == Never.self:

if type(of: p.a) == Never.self { print("done") }

I tried to build a protocol based dynamic dispatch to distinguish Never vs the rest. The implication is that types that have a: Never will need to have a different protocol requirement. Something that I'm not very happy with since it will rely on runtime crashes to report incorrect API implementations (as opposed to a compiler error).

I’m not sure what you mean by this question. Never cannot be instantiated, so if p.a is Never is never true, just like if 1 == 2 is never true. Moreover, you ask about testing the dynamic type but then you state you want a compiler error, and these are mutually contradictory. Perhaps step back and describe what you’re trying to do?

so if p.a is Never is never true

When using the types from the original question:

func testPA<T: P>(p: T) {
    // It is not known if p.a is Never or not
    // Is there a way to test it?
    // `p.a is Never` will not return if p.a is never
    if p.a is Never { /* don't use p.a */ }
    else { p.a }
}
testPA(p: R()) // fine, r.a is not Never
testPA(p: S()) // crash, s.a is Never
  • p.a can be of type Never
  • It can be unknown if p.a is Never or not, given the current context

It would be useful to be able to test if p.a is Never, without retrieving its value.

Moreover, you ask about testing the dynamic type but then you state you want a compiler error

When looking for a way to test if p.a is Never, I've tried to use dynamic dispatch based on the protocol the type adheres too.

This can be achieved, but with a downside: when writing a new type of P, you must remember that <iff a is of type Never, don't use P but use OtherProtocol>.
If P is used where OtherProtocol should be used, it will lead to a runtime crash (the error message could be something like "use OtherProtocol for types that have a: Never").
If OtherProtocol is used where P should be used the error is likely to go undetected.

I'd rather avoid such intricacies in API design if possible.

If OtherProtocol is used where P should be used the error is likely to go undetected.

Actually, this case can be defined away by using

protocol OtherProtocol: P where A == Never {}

This is incorrect. No value can have type Never.

In your example, when the associated type is Never, attempting to access p.a will never succeed. The body of testPA is equivalent in all respects to just writing p.a.

You could, in the body of testPA, test if T.A is (statically) equal to Never. But the dynamic type of p.a can never be Never.

I hope this helps you understand what’s going on a little better.

1 Like

Thank you Xiaudi, your help is appreciated.

However I think it is incorrect to state that the dynamic type of p.a can never be Never.
When considering the testPA example, a dynamic type of p exists while executing testPA. When the dynamic type of p is S the dynamic type of p.a is Never.
Even though the value of p.a can not be created.

The swift runtime also has (or could have) this information available.

Now that dynamic information may not be accessible.
I think it could be though. The trick would be to not instantiate any type that is uninhabitable during type queries and instead return the uninhabitable type.

There cannot be a dynamic type of something that has no runtime existence. This is not a technical limitation but fundamental to the concept of a dynamic type.

I'm talking about two different things, and not separating them clearly enough:

  • The dynamic type of p
  • The static type of P.a

To restate the question more clearly, can I ask the runtime if the static type of the dynamic p has a property typed Never?

Sure—I assume you mean you want to know about the dynamic type of p; otherwise, I'm sure how to interpret the phrase "static type of a dynamic p." You could, for instance, create a protocol requirement of P that tests if A equals Never:

protocol P {
    associatedtype A
    var isNeverA: Bool { get }
}

extension P {
    var isNeverA: Bool { A.self == Never.self }
}

I'm just not sure why you specifically need the dynamic type of p for this purpose (necessitating the protocol requirement for dynamic dispatch), or why you specifically want to ask the runtime about A—neither seems necessary given that you're working with structs and generic functions.

Leaving aside the question of whether you should do it this way, you can write this test dynamically by comparing the types:

if T.A.self == Never.self { ... }
1 Like

Note: T is the static type of p, and @berik states they want the dynamic type of p (and to test dynamically the value of A, which you do demonstrate). Again, I'm not sure why in their scenario but if that's absolutely the requirement then T won't cut it.

1 Like

I’m optimistically assuming this is a communication problem… although I confess I also assumed that associated types can’t be fulfilled covariantly by subtypes, which turns out not to be correct, so this approach might be genuinely inadequate in a sufficiently special case if Never becomes a true bottom type.

2 Likes

Thank you Jayton!

With your suggestion it is easy to get the "generic dispatch" example working as I wanted:

struct PWrapper<T: P> {
    let p: T

    func f() {
        if T.A.self == Never.self {
            print("done")
        } else {
            PWrapper<T.A>(p: p.a).f()
        }
    }
}

// Either of these print "done"
PWrapper(p: S()).f()
PWrapper(p: R()).f()

Or when using the (simpler) function based approach:

// Public
protocol P {
    associatedtype A: P
    var a: Self.A { get }
}

extension Never: P {
    var a: Never { fatalError() }
}

struct R<T: P>: P {
    let a: T
}

struct S: P {
    var a: Never { fatalError() }
}

// Internal
extension P {
    func nestingLevel() -> Int {
        if A.self == Never.self { return 0 }
        return a.nestingLevel() + 1
    }
}

func testPA<T: P>(p: T) {
    print("Call testPA on \(type(of: p))")
    if T.A.self == Never.self { /* don't use p.a */ }
    else { testPA(p: p.a) }
}

let nested = R(a: R(a: R(a: S())))

// Prints
//    Call testPA on R<R<R<S>>>
//    Call testPA on R<R<S>>
//    Call testPA on R<S>
//    Call testPA on S
testPA(p: nested)

// Returns 3
nested.nestingLevel()

It is clear that you have some reservations implementing the conditional this way. May I ask why?