Method dispatching issue with subclasses implementing Equatable protocol.


(Francisco Javier Fernández Toro) #1

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?

Check the following code (
https://gist.github.com/fjfdeztoro/a5097f1b24379e127674eb1df8c97d96):

class Superclass : Equatable {
    let foo: Int

    init(foo: Int) { self.foo = foo }

    func equal(to: Superclass) -> Bool {
        return foo == to.foo
    }
}

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)
    }
}

func == (lhs: Subclass, rhs: Subclass) -> Bool {
    return lhs.equal(to: rhs)
}

class SubclassWithDifferentOperator: Subclass {}

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

let x = SubclassWithDifferentOperator(foo: 1, bar: 1)
let y = SubclassWithDifferentOperator(foo: 1, bar: 2)

(x == y) != (x != y) // Prints: True

···

---

Fran Fernandez.


(Joe Groff) #2

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

···

On Jan 18, 2017, at 2:59 AM, Francisco Javier Fernández Toro via swift-evolution <swift-evolution@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?


(Francisco Javier Fernández Toro) #3

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@apple.com> wrote:

> On Jan 18, 2017, at 2:59 AM, Francisco Javier Fernández Toro via > swift-evolution <swift-evolution@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


(Tony Allevato) #4

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@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@apple.com> wrote:

> On Jan 18, 2017, at 2:59 AM, Francisco Javier Fernández Toro via > swift-evolution <swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Francisco Javier Fernández Toro) #5

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@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@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@apple.com> wrote:

> On Jan 18, 2017, at 2:59 AM, Francisco Javier Fernández Toro via >> swift-evolution <swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Tony Allevato) #6

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@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@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@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@apple.com> wrote:

> On Jan 18, 2017, at 2:59 AM, Francisco Javier Fernández Toro via > swift-evolution <swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Francisco Javier Fernández Toro) #7

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)

···

On Wed, Jan 18, 2017 at 6:58 PM, Tony Allevato <tony.allevato@gmail.com> wrote:

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@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@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@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@apple.com> wrote:

> On Jan 18, 2017, at 2:59 AM, Francisco Javier Fernández Toro via >> swift-evolution <swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Pierre Monod-Broca) #8

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 <swift-evolution@swift.org> a écrit :

On Wed, Jan 18, 2017 at 6:58 PM, Tony Allevato <tony.allevato@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@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@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@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@apple.com> wrote:

> On Jan 18, 2017, at 2:59 AM, Francisco Javier Fernández Toro via swift-evolution <swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Goffredo Marocchi) #9

Ah... we will be missing hiding behind the warm embrace of message passing/dynamic dispatch :stuck_out_tongue:

···

Sent from my iPhone

On 20 Jan 2017, at 19:24, Pierre Monod-Broca via swift-evolution <swift-evolution@swift.org> 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 <swift-evolution@swift.org> a écrit :

On Wed, Jan 18, 2017 at 6:58 PM, Tony Allevato <tony.allevato@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@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@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@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@apple.com> wrote:

> On Jan 18, 2017, at 2:59 AM, Francisco Javier Fernández Toro via swift-evolution <swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Karl) #10

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 <swift-evolution@swift.org> 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 <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> a écrit :

On Wed, Jan 18, 2017 at 6:58 PM, Tony Allevato <tony.allevato@gmail.com <mailto:tony.allevato@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@gokarumi.com <mailto:fran@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@gmail.com <mailto:tony.allevato@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@swift.org <mailto:swift-evolution@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@apple.com <mailto:jgroff@apple.com>> wrote:

> On Jan 18, 2017, at 2:59 AM, Francisco Javier Fernández Toro via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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 <http://bugs.swift.org/> about that?

-Joe

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Pierre Monod-Broca) #11

It's worth mentioning that with equal(to:) the dispatch is dynamic only on `self` not on `to`, which means different implementation can be called if you swap the operands. For exemple:

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

a.equal(to: b) // calls Super.equal(to:)
b.equal(to: a) // calls Sub.equal(to:)

Pierre

···

Le 21 janv. 2017 à 08:20, Karl Wagner <razielim@gmail.com> a écrit :

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 <swift-evolution@swift.org> 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 <swift-evolution@swift.org> a écrit :

On Wed, Jan 18, 2017 at 6:58 PM, Tony Allevato <tony.allevato@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@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@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@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@apple.com> wrote:

> On Jan 18, 2017, at 2:59 AM, Francisco Javier Fernández Toro via swift-evolution <swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution