[swift-evolution] Method dispatching issue with subclasses implementing Equatable protocol.

Francisco Javier Fernández Toro fran at gokarumi.com
Fri Jan 20 03:45:17 CST 2017


On Wed, Jan 18, 2017 at 6:58 PM, Tony Allevato <tony.allevato at gmail.com>
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), 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 <
> fran at gokarumi.com> 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 <tony.allevato at gmail.com>
>> 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>, 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 <swift-evolution at swift.org> 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 <jgroff at apple.com> wrote:
>>
>>
>> > On Jan 18, 2017, at 2:59 AM, Francisco Javier Fernández Toro via
>> swift-evolution <swift-evolution at swift.org> 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 about that?
>>
>> -Joe
>>
>>
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>>
>>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170120/610c642c/attachment.html>


More information about the swift-evolution mailing list