This is very interesting and all Swift developers will need to be aware of it, 
if it isn’t a bug.

One thing that I think is a little worrying is that if, rather than the == 
operator, you had a CustomEquatable protocol which defined the equal(to:) 
function, that function would be dynamically dispatched. You would think that 
operator witnesses should be resolved in the same way.

I believe that should even work for the dynamic Self; we have already verified 
at compile-time that an overload exists (even though it may not be the most 
appropriate one to execute). So you would get:

let a = Sub()
let b = Sub()
let c = Super()

(a as Super) == b // Both values have (dynamic) type Sub, calls ==(Sub,Sub)
a == c                   // One type is not a Sub, must compare using ==(Super, 
Super)

- Krl

> On 20 Jan 2017, at 20:24, Pierre Monod-Broca via swift-evolution 
> <[email protected]> wrote:
> 
> The way I understand it, it's a bad idea to override == and != (or any infix 
> operator) for Sub if Super has them and that's why the default implementation 
> from Equatable only generates !=(Super, Super) and not !=(Sub, Sub) (and 
> there is no ==(Sub, Sub) generated either).
> 
> And it's a bad idea because (without dynamic dispatch on both operands) it 
> leads to unexpected behavior.
> 
> Considering :
> ```
> func ==(lhs: Super, rhs: Super) -> Bool {
>     print("Super")
>     return true
> }
> 
> func ==(lhs: Sub, rhs: Sub) -> Bool {
>     print("Sub")
>     return false
> }
> 
> let a = Sub()
> let b = Sub()
> a == b // Sub
> a as Super == b // Super
> a == b as Super // Super
> à as Super == b as Super // Super
> ```
> 
> One would compare the same objects and don't get the same result.
> 
> Instead you have to check the dynamic type yourself.
> 
> 
> Pierre
> 
> Le 20 janv. 2017 à 10:45, Francisco Javier Fernández Toro via swift-evolution 
> <[email protected] <mailto:[email protected]>> a écrit :
> 
>> 
>> 
>> On Wed, Jan 18, 2017 at 6:58 PM, Tony Allevato <[email protected] 
>> <mailto:[email protected]>> wrote:
>> Ok, this actually does feel a bit strange. The behavior you're seeing seems 
>> to be a consequence of 
>> [SE-0091](https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md
>>  
>> <https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md>),
>>  but it looks like you're seeing different behavior than what I described in 
>> the "Class types and inheritance" section of that proposal.
>> 
>> If Sub has `==(Sub, Sub)` implemented as a *static* function, I just tried 
>> it and it's *ignored* (`==(Super, Super)` gets called instead), even when 
>> the two actual arguments are known to be statically of type Sub. I think 
>> this is because of the way that proposal was implemented: when it sees that 
>> `Sub` extends `Super`, which conforms to `Equatable`, it appears that it's 
>> only looking for static overloads of `==` that are satisfied at the *point 
>> of conformance*, which would be `==(Super, Super)` (because `Super` conforms 
>> to `Equatable where Self == Super`). The wording of the proposal makes this 
>> case: "Then, we say that we do not consider an operator function if it 
>> implements a protocol requirement, because the requirement is a 
>> generalization of all of the operator functions that satisfy that 
>> requirement."
>> 
>> Contrarily, if you provide `==(Sub, Sub)` as a global function instead of a 
>> static one, it *does* get called. I think in this case, the type checker 
>> gets the whole set of candidate operators (which, unlike above, includes the 
>> global `==(Sub, Sub)`), and it gets used because it's a more specific match?
>> 
>> 
>> FWIW, I've just changed both `==` functions to make them global, the the 
>> outcome is still the same, its using `==(Super,Super)` to resolve 
>> `!=(Sub,Sub)
>>  
>> Can someone from the core team chime in and say whether this is intentional 
>> behavior? It feels wrong that simply changing the location where the 
>> operator is defined would change the behavior like this.
>> 
>> FWIW, to avoid these sharp edges, there's no need to implement `==` for 
>> subtypes; since you have to use an overridable `equals` method anyway, just 
>> have the base type implement `==` to delegate to it, and then have subtypes 
>> override `equals` alone.
>> 
>> 
>> On Wed, Jan 18, 2017 at 9:36 AM Francisco Javier Fernández Toro 
>> <[email protected] <mailto:[email protected]>> wrote:
>> Yeah guys, you are right, my code is busted, I was trying to point something 
>> different out:
>> 
>> The next code is showing the possible issue. In theory to make a class 
>> Equatable, you just have to mark it with the Equatable protocol and 
>> implement `==` as a static function or as a global one.
>> 
>> If you don't override the equal method and you just invoke your super class 
>> equality method you'll get something like this: 
>> 
>> ```
>> class Superclass : Equatable {
>>     let foo: Int
>>     
>>     init(foo: Int) { self.foo = foo }
>>     
>>     func equal(to: Superclass) -> Bool {
>>         return foo == to.foo
>>     }
>>     
>>     static func == (lhs: Superclass, rhs: Superclass) -> Bool {
>>         return lhs.equal(to: rhs)
>>     }
>> }
>> 
>> class Subclass: Superclass {
>>     let bar: Int
>>     init(foo: Int, bar: Int) {
>>         self.bar = bar
>>         super.init(foo: foo)
>>     }
>>     
>>     func equal(to: Subclass) -> Bool {
>>         return bar == to.bar && super.equal(to: to)
>>     }
>>     
>>     static func == (lhs: Subclass, rhs: Subclass) -> Bool {
>>         return lhs.equal(to: rhs)
>>     }
>> }
>> 
>> class SubclassWithDifferentOperator: Subclass {
>>     static func != (lhs: SubclassWithDifferentOperator, rhs: 
>> SubclassWithDifferentOperator) -> Bool {
>>         return !(lhs.equal(to: rhs))
>>     }
>> }
>> 
>> let a = Subclass(foo: 1, bar: 1)
>> let b = Subclass(foo: 1, bar: 2)
>> 
>> (a == b) != (a != b) // Prints: false, not expected
>> 
>> let x = SubclassWithDifferentOperator(foo: 1, bar: 1)
>> let y = SubclassWithDifferentOperator(foo: 1, bar: 2)
>> 
>> (x == y) != (x != y) // Prints: true, expected
>> ```
>> 
>> So, after adding a couple of `print` statement in those equal method what I 
>> can see is that for Subclass, when you are need to call `!=` what Swift is 
>> doing is using `func ==(Superclass, Superclass)` and apply `!` as Tony has 
>> pointed out.
>> 
>> What I cannot understand is why is not using `func == (Subclass, Subclass)`
>> 
>> I hope it makes more sense now.
>> 
>> ---
>> Fran Fernandez
>> 
>> On Wed, Jan 18, 2017 at 6:13 PM, Tony Allevato <[email protected] 
>> <mailto:[email protected]>> wrote:
>> This seems to work for me:
>> 
>> ```
>> class Super: Equatable { 
>>     let x: Int
>>     init(x: Int) {
>>         self.x = x
>>     }
>>     func equals(_ rhs: Super) -> Bool { 
>>         return x == rhs.x 
>>     } 
>>     static func ==(lhs: Super, rhs: Super) -> Bool { 
>>         return lhs.equals(rhs) 
>>     } 
>> } 
>> 
>> class Sub: Super {
>>     let y: Int
>>     init(x: Int, y: Int) {
>>         self.y = y
>>         super.init(x: x)
>>     }
>>     override func equals(_ rhs: Super) -> Bool {
>>         if let rhs = rhs as? Sub {
>>             return y == rhs.y && super.equals(rhs)
>>         }
>>         return false
>>     }   
>> }
>> 
>> let a = Sub(x: 1, y: 1)
>> let b = Sub(x: 1, y: 2)
>> let c = Sub(x: 1, y: 1)
>> 
>> a == b  // false, expected
>> a == c  // true, expected
>> a != b  // true, expected
>> a != c  // false, expected
>> ```
>> 
>> Additionally, when I made the change Joe suggested, your code also worked, 
>> so maybe there was an error when you updated it?
>> 
>> FWIW, the default implementation of != just invokes !(a == b) 
>> <https://github.com/apple/swift/blob/master/stdlib/public/core/Equatable.swift#L179-L181
>>  
>> <https://github.com/apple/swift/blob/master/stdlib/public/core/Equatable.swift#L179-L181>>,
>>  so I believe it's *impossible* (well, uh, barring busted RAM or processor I 
>> guess) for it to return the wrong value for the same arguments if you only 
>> implement ==.
>> 
>> 
>> 
>> On Wed, Jan 18, 2017 at 8:52 AM Francisco Javier Fernández Toro via 
>> swift-evolution <[email protected] 
>> <mailto:[email protected]>> wrote:
>> Thank you for your answer Joe,
>> 
>> you are right the equal(to:) wasn't a valid override, but even after using 
>> the one you've proposed, the behavior is not the expected one
>> 
>> 
>> let a = Subclass(foo: 1, bar: 1)
>> let b = Subclass(foo: 1, bar: 2)
>> 
>> (a == b) != (a != b) // Prints true
>> 
>> let x = SubclassWithDifferentOperator(foo: 1, bar: 1)
>> let y = SubclassWithDifferentOperator(foo: 1, bar: 2)
>> 
>> (x == y) != (x != y) // Prints false
>> 
>> As you can see above if a subclass does not implement the global function 
>> !=, the equal operation seems to be broken.
>> 
>> ---
>> 
>> Fran Fernandez
>> 
>> On Wed, Jan 18, 2017 at 5:44 PM, Joe Groff <[email protected] 
>> <mailto:[email protected]>> wrote:
>> 
>> > On Jan 18, 2017, at 2:59 AM, Francisco Javier Fernández Toro via 
>> > swift-evolution <[email protected] 
>> > <mailto:[email protected]>> wrote:
>> >
>> > Hi,
>> >
>> > I've found that when you have a class hierarchy which implements 
>> > Equatable, if you want to have the != operator working as expected, you 
>> > need to override it, it's not enough with ==.
>> >
>> > If you don't define you own subclass != operator, Swift compiler will use 
>> > the super class to resolve that operation.
>> >
>> > Is there any reason for that?
>> 
>> The `equal(to:)` method inside `Subclass` is not a valid override of 
>> `Superclass` because its argument only accepts `Subclass` instances, but the 
>> parent method needs to work with all `Superclass` instances. If you write it 
>> as an override, it should work:
>> 
>> class Subclass: Superclass {
>>     let bar: Int
>>     init(foo: Int, bar: Int) {
>>         self.bar = bar
>>         super.init(foo: foo)
>>     }
>> 
>>     override func equal(to: Superclass) -> Bool {
>>       if let toSub = to as? Subclass {
>>         return bar == toSub.bar && super.equal(to: to)
>>       }
>>       return false
>>     }
>> }
>> 
>> We should probably raise an error, or at least a warning, instead of 
>> silently accepting your code as an overload. Would you be able to file a bug 
>> on bugs.swift.org <http://bugs.swift.org/> about that?
>> 
>> -Joe
>> 
>> _______________________________________________
>> swift-evolution mailing list
>> [email protected] <mailto:[email protected]>
>> https://lists.swift.org/mailman/listinfo/swift-evolution 
>> <https://lists.swift.org/mailman/listinfo/swift-evolution>
>> 
>> 
>> _______________________________________________
>> swift-evolution mailing list
>> [email protected] <mailto:[email protected]>
>> https://lists.swift.org/mailman/listinfo/swift-evolution 
>> <https://lists.swift.org/mailman/listinfo/swift-evolution>
> _______________________________________________
> swift-evolution mailing list
> [email protected]
> https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution
  • [swift-evolution] Met... Francisco Javier Fernández Toro via swift-evolution
    • Re: [swift-evolu... Joe Groff via swift-evolution
      • Re: [swift-e... Francisco Javier Fernández Toro via swift-evolution
        • Re: [swi... Tony Allevato via swift-evolution
          • Re: ... Francisco Javier Fernández Toro via swift-evolution
            • ... Tony Allevato via swift-evolution
              • ... Francisco Javier Fernández Toro via swift-evolution
                • ... Pierre Monod-Broca via swift-evolution
                • ... Goffredo Marocchi via swift-evolution
                • ... Karl Wagner via swift-evolution
                • ... Pierre Monod-Broca via swift-evolution

Reply via email to