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)?