“All paths through this function will call itself” on Protocol Equality Method

I have a protocol, ViewEdgeConvertible, that’s used to convert between directional and fixed view edges using a UITraitCollection:

public protocol ViewEdgeConvertible {

    func fixedViewEdge(
        using traitCollection: UITraitCollection?
    ) -> FixedViewEdge

    func directionalViewEdge(
        using traitCollection: UITraitCollection?
    ) -> DirectionalViewEdge

}

public enum FixedViewEdge: Equatable {

    case top
    case left
    case right
    case bottom

}

public enum DirectionalViewEdge: Equatable {

    case top
    case leading
    case trailing
    case bottom

}

Using this, I can convert freely between the two types given a trait collection, which lets me account for right-to-left layouts. But when I try to make an == implementation:

public func == (lhs: ViewEdgeConvertible, rhs: ViewEdgeConvertible) -> Bool {
    return lhs.fixedViewEdge(using: nil) == rhs.fixedViewEdge(using: nil)
}

I get this warning: All paths through this function will call itself.

It looks like the compiler sees the == method between the two FixedViewEdge instances and defaults to this method I’ve implemented. Is there a way to write this that takes advantage of the synthesized Equatable conformance on the FixedViewEdge enum?

Perhaps make lhs and rhs generic types that are constrained to ViewEdgeConvertible and Equatable?

This has the same result (and results in an infinite loop at runtime):

public func == <T: ViewEdgeConvertible>(
    lhs: T,
    rhs: T
) -> Bool where T: Equatable {
    return lhs.fixedViewEdge(using: nil) == rhs.fixedViewEdge(using: nil)
}

This works but is ugly:

public func == (lhs: ViewEdgeConvertible, rhs: ViewEdgeConvertible) -> Bool {
    switch (lhs.fixedViewEdge(using: nil), rhs.fixedViewEdge(using: nil)) {
    case (.top, .top), (.left, .left), (.right, .right), (.bottom, .bottom):
        return true
    default:
        return false
    }
}

FWIW, I am not seeing the error in a playground running in Xcode 12 beta 4.

To reproduce the issue you need to add the implementation that makes the types conform to ViewEdgeConvertible:

extension FixedViewEdge: ViewEdgeConvertible {

    public func fixedViewEdge(
        using traitCollection: UITraitCollection?
    ) -> FixedViewEdge {
        self
    }

    public func directionalViewEdge(
        using traitCollection: UITraitCollection?
    ) -> DirectionalViewEdge {
        switch (self, traitCollection?.layoutDirection) {
        case (.top, _): return .top
        case (.left, .rightToLeft): return .trailing
        case (.left, _): return .leading
        case (.right, .rightToLeft): return .leading
        case (.right, _): return .trailing
        case (.bottom, _): return .bottom
        }
    }

}

extension DirectionalViewEdge: ViewEdgeConvertible {

    public func fixedViewEdge(
        using traitCollection: UITraitCollection?
    ) -> FixedViewEdge {
        switch (self, traitCollection?.layoutDirection) {
        case (.top, _): return .top
        case (.leading, .rightToLeft): return .right
        case (.leading, _): return .left
        case (.trailing, .rightToLeft): return .left
        case (.trailing, _): return .right
        case (.bottom, _): return .bottom
        }
    }

    public func directionalViewEdge(
        using traitCollection: UITraitCollection?
    ) -> DirectionalViewEdge {
        self
    }

}

Thanks, I'm now able to reproduce! Here's a slightly condensed version of your workaround which also silences the error:

public func == (lhs: ViewEdgeConvertible, rhs: ViewEdgeConvertible) -> Bool {
    switch lhs.fixedViewEdge(using: nil) {
    case rhs.fixedViewEdge(using: nil):
        return true
    default:
        return false
    }
}

ETA: In any case, I would recommend filing a bug on bugs.swift.org

1 Like

Nice!

You can also implement FixedViewEdge.== to bump its priority:

public func ==(lhs: FixedViewEdge, rhs: FixedViewEdge) -> Bool {
    switch (lhs, rhs) {
    case (.top, .top), (.bottom, .bottom), (.left, .left), (.right, .right): return true
    default: return false
    }
}

You probably also want to do that on DirectionalViewEdge so that directionalViewEdge1 == directionalViewEdge2 use the more performant Equatable implementation. (or maybe it'll get constant-folded anyway).

Personally, I'm not sure it's advisable to implement == over heterogeneous types.

3 Likes

Filed as SR-13356.

I think you need to move the method into an extension on ViewEdgeConvertible, rather than keep it as a free function:

extension ViewEdgeConvertible {
    public static func == (lhs: ViewEdgeConvertible, rhs: ViewEdgeConvertible) -> Bool {
        return lhs.fixedViewEdge(using: nil) == rhs.fixedViewEdge(using: nil)
    }
}

That should silence the warning for you.

2 Likes

Thank you, this does indeed silence the warning!

That's very spooky, that a free function == and a static function ViewEdgeConvertible.== have different priorities (one below synthesized == and one above). :scream:

Using the extension does satisfy the warning, but now I can’t seem to use the method. In a test, I have this line:

XCTAssertTrue(edge == edge)

And this fails to compile with the error Binary operator '==' cannot be applied to two 'ViewEdgeConvertible' operands. This is what I have for ==:

extension ViewEdgeConvertible {

    public static func == (
        lhs: ViewEdgeConvertible,
        rhs: ViewEdgeConvertible
    ) -> Bool {
        return lhs.fixedViewEdge(using: nil) == rhs.fixedViewEdge(using: nil)
    }

}

Using the workaround from @Jumhyn, I can at least call == from a test. Very odd.