[swift-evolution] Method dispatching issue with subclasses implementing Equatable protocol.
Tony Allevato
tony.allevato at gmail.com
Wed Jan 18 11:58:16 CST 2017
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?
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/20170118/dce3169f/attachment.html>
More information about the swift-evolution
mailing list