tera
1
Found an interesting corner in Swift:
infix operator <->
class Base {
static func <->(_ lhs: Base, _ rhs: Base) -> Int {
print("base implementation"); return 0
}
}
class Derived: Base {
static func <->(_ lhs: Derived, _ rhs: Base) -> Int {
print("derived implementation-1"); return 1
}
static func <->(_ lhs: Base, _ rhs: Derived) -> Int {
print("derived implementation-2"); return 1
}
static func <->(_ lhs: Derived, _ rhs: Derived) -> Int {
print("derived implementation-3"); return 2
}
}
let base = Base()
let derived = Derived()
let derivedAsBase: Base = Derived()
_ = base <-> derived
_ = derived <-> base
_ = derived <-> derivedAsBase
Guess what's being printed.
Base implementation in all three cases!
But why?!
tera
2
This is somewhat related but more important as it is about EQ / Hashability rather than using a custom operator:
class A: Hashable {
var x = 0
init(x: Int = 0) { self.x = x }
func hash(into hasher: inout Hasher) {
hasher.combine(x)
}
func eq(_ other: A) -> Bool {
x == other.x
}
static func == (lhs: A, rhs: A) -> Bool {
lhs.eq(rhs)
}
}
class B: A {
var y = 0
init(x: Int = 0, y: Int = 0) { self.y = y; super.init(x: x) }
override func hash(into hasher: inout Hasher) {
super.hash(into: &hasher)
hasher.combine(y)
}
override func eq(_ other: A) -> Bool {
if let b = other as? B {
super.eq(other) && y == b.y
} else {
super.eq(other)
}
}
static func == (lhs: B, rhs: B) -> Bool {
lhs.eq(rhs)
}
static func == (lhs: B, rhs: A) -> Bool {
lhs.eq(rhs)
}
static func == (lhs: A, rhs: B) -> Bool {
rhs.eq(lhs)
}
}
var a = A()
var ab: A = B()
var b = B()
precondition(a == ab) // ✅
precondition(ab == a) // ✅
precondition(a == b) // ✅
precondition(b == a) // ✅
precondition(a.hashValue == ab.hashValue) // ❌
precondition(a.hashValue == b.hashValue) // ❌
This seemingly "doing everything right" implementation breaks hash invariant (equal things must have same hash values).
xwu
(Xiaodi Wu)
3
This implementation of == doesn't "do everything right" because what you are trying to express violates transitivity: two distinct instances of B named b1 and b2 can compare equal to a but compare not equal to each other.
2 Likes
tera
4
Is there "the right way" here? Or, as with floating point, we obey one rule (IEEE semantics) to break another (a == a).
Say, I want to correct the above code and make subclasses always not equal to base classes. transitivity rule saved, hurrah! But then Liskov substitution principle is broken as I won't be able replacing an object of a class with an object of a subclass without breaking the app (the chosen object previously compared equal to its peers and subclass won't).