Compiler fails to use the more specialised implementation

(I'm not sure if the title describes the problem accurately. Please help me change it if it doesn't.)

I have the following Comparable extensions:

//	Extension 1
extension Comparable where Self: Strideable {
	///	Inspects if this value is right next to the given other value.
	///	- Parameter other: The given other value.
	///	- Returns: `true` if the 2 values are right next to each other, `false` otherwise.
	public func borders(on other: Self) -> Bool {
		self.separates(from: other, byDegrees: 1)
	}
	
	///	Inspects the correctness of the Bacon number between this value and the given other value.
	///	- Parameters:
	///	  - other: The given other value.
	///	  - degrees: The Bacon number to test for.
	///	- Returns: `true` if the Bacon number is correct, `false` otherwise.
	public func separates(from other: Self, byDegrees degrees: Self.Stride) -> Bool {
		other == self.advanced(by: degrees) || self == other.advanced(by: degrees)
	}
}

//	Extension 2
extension Comparable {
	///	Inspects if the value is right next to the given other value.
	///	- Parameter other: The given other value.
	///	- Returns: `true` if the 2 values are right next to each other, `false` otherwise.
	public func borders(on other: Self) -> Bool {
		return false
	}
}

The extensions define a new method that checks if 2 values are right next to each other. It works as intended for most of the times:

0.borders(on: 1)     // true
1.5.borders(on: 2.3) // false
"a".borders(on: "b") // false

It stops working correctly when the values being compared have the placeholder type within a generic type:

struct Pair<T: Comparable> {
	let value1: T
	let value2: T
	var isAPairOfNeighbours: Bool { value1.borders(on: value2) }
}

let pairOf0And1 = Pair(value1: 0, value2: 1)
type(of: pairOf0And1) // __lldb_expr_30.Pair<Int>.Type

type(of: pairOf0And1.value1) // Int.Type
type(of: pairOf0And1.value2) // Int.Type

pairOf0And1.isAPairOfNeighbours                    // false, but I expect it to be true
pairOf0And1.value1.borders(on: pairOf0And1.value2) // true

I think the cause of the problem is that within Pair<T>, the compiler only knows that value1 and value2 are Comparable, and it doesn't know if they're also Strideable. Is this true, and is this the correct behaviour? How do I work around this problem?

You're on the right track regarding the issue here. It's really a combination of two facts. The first, as you mention, is that T in Pair<T> is constrained only to Comparable. The second is that members defined in protocol extensions (rather than being defined as protocol requirements) receive static dispatch, so the compiler must resolved which implementation of borders(on:) gets called at time isAPairOfNeighbors is compiled. Since T may or may not conform to Strideable, the only legal implementation to call is the one in the unconstrained Comparable extension.

The general advice to achieve dynamic dispatch here is "use a protocol requirement." E.g., the following example prints R & Q since compiler correctly selects foo from extension R where Self: Q as the witness for P.foo at the point where the conformance X: P is declared:

protocol P {
    func foo()
}

protocol R {}
protocol Q {}

extension R {
    func foo() { print("R") }
}

extension R where Self: Q {
    func foo() { print("R & Q") }
}

struct S<T: P> {
    let t: T
    func doFoo() {
        t.foo()
    }
}

struct X: R, Q, P {}

S(t: X()).doFoo() // "R & Q"

The obvious downside is that this prevents you from simply "piggybacking" on existing protocols, since each type you want to use with S will have to declare its conformance to P explicitly.

(I believe this captures the relationships in your post appropriately where Q <-> Strideable, R <-> Comparable, S <-> Pair, and P is a newly defined protocol to achieve dynamic dispatch, but if I've missed something let me know.)

1 Like