Assume equality regardless of order of lhs and rhs

Hello,

Currently when implementing equality operator == the order of lhs and rhs arguments seems to matter and compiler is not able to find the implementation when arguments are flipped. I'd assume that if lhs == rhs, then rhs == lhs. Maybe there are caveats to making such assumption. Not sure.

Consider the following example:

struct SwiftVersion: RawRepresentable {
    var rawValue: String

    init(rawValue: String) {
        self.rawValue = rawValue
    }

    static func == (lhs: SwiftVersion, rhs: String) -> Bool {
        return lhs.rawValue == rhs
    }
}

let swift_5_5 = SwiftVersion(rawValue: "5.5")

if swift_5_5 == "5.5" {
    print("This is Swift 5.5")
}

if "5.5" == swift_5_5 {
    print("This is Swift 5.5")
}

Note that the second equality test in yoda-style does not compile and errors with the following message:

error: binary operator '==' cannot be applied to operands of type 'String' and 'SwiftVersion'
note: overloads for '==' exist with these partially matching parameter lists: (String, String)

In my opinion it would make things simpler for developers, if compiler was able to derive implementation, i.e either automatically add one that mirrors the order of values or call the existing one providing arguments in a reverse order.

// Auto-generated
extension SwiftVersion {
    static func == (lhs: String, rhs: SwiftVersion) -> Bool {
        return rhs.rawValue == lhs
    }
}
1 Like

The compiler has no special knowledge that the equality operator is commutative. In fact, it doesn't have any semantic knowledge of the == operator at all. It's just another binary operator with some precedence and associativity of arguments defined.

3 Likes

This strikes me as extremely odd. You did not really define equality here (as the title implies), you wrote an operator == that kind of just happens to be named the same as the one the Equality protocol demands.

As it has two differently typed parameters it makes perfectly sense that the second if does not compile.
I agree that with your naming it looks weird at the call site, but making the compiler synthesize a method with switched parameters would be an extremely limited use case, I guess.
I assume you'd want it to be limited to ==, but naturally only for versions that have differing types for lhs and rhs. I want to frame challenge this here: Having a == operator with different types is a questionable idea in the first place imo, because it is super atypical. When I read if a == b I assume that they are of the same type (or share the same protocol or whatever). I'd dislike any API that "tricks" me like this...

2 Likes

Rather than an equality comparison with two differently typed operands, consider conforming SwiftVersion to ExpressibleByStringLiteral and Equatable like so:

extension SwiftVersion: ExpressibleByStringLiteral {
    init(stringLiteral: String) { self.rawValue = stringLiteral }
}

extension SwiftVersion: Equatable {} // compiler-generated `==` is good

This way you can still compare to string literals like swift_5_5 == "5.5" or "5.5" != someVersion. But for values of type String you will need to call init(rawValue:) though.

4 Likes

Nitpick. Walks like a duck, quacks like a duck, probably a duck.

I agree that it could be limited. Just to make it clear though, the compiler does not have to synthesize anything, it could just call the same implementation with operands given in different order. Btw Comparable synthesizes a lot stuff, you would not believe it.

Now when you mention that, maybe it does not worth it all. I could always take .rawValue and compare it or add a method. Just not as "cool" I guess.

Fun fact implementing ExpressibleByStringLiteral as suggested by @pyrtsa actually achieves exactly that, i.e it looks like equality there works both ways.

Not only that, doing it like that you're indeed comparing equality of two SwiftVersion values, not mixed types.

Ah you're right, it actually instantiates the type.

Plus you are getting automatic != support.