Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift 2.0: Parametrized classes don't call proper == function if it inherits from class that is Equatable

When a parametrized class inherits from another class conforming to Equatable, == calls the superclass's == . Can anyone explain why this is happening and/or how I may be doing something wrong here? I believe an example best illustrates my issue:

public class Foo: Equatable {}
public func ==(lhs: Foo, rhs: Foo) -> Bool { return false }

//Parametrized
public class Bar<T: Equatable>: Foo {
  public var bar: T?
  public init(barIn: T?) {
    self.bar = barIn
  }
}
public func ==<T>(lhs: Bar<T>, rhs: Bar<T>) -> Bool { return lhs.bar == rhs.bar }

//Non parametrized
public class Baz: Foo {
  public var baz: Int?
  public init(bazIn: Int?) {
    self.baz = bazIn
  }
}
public func ==(lhs: Baz, rhs: Baz) -> Bool { return lhs.baz == rhs.baz }

//Parametrized, no inheritance
public class Qux<T: Equatable>: Equatable {
  public var qux: T?
  public init(quxIn: T?) {
    self.qux = quxIn
  }
}
public func ==<T>(lhs: Qux<T>, rhs: Qux<T>) -> Bool { return lhs.qux == rhs.qux }

Bar<Int>(barIn: 1) == Bar<Int>(barIn: 1) //false
Baz(bazIn: 1) == Baz(bazIn: 1) //true
Qux(quxIn: 1) == Qux(quxIn: 1) //true, of course
like image 309
Kenta Labur Avatar asked Dec 07 '15 23:12

Kenta Labur


2 Answers

Though I didn't find anything on the Swift reference about this, this gives us a clue:

Generics are lower down the pecking order. Remember, Swift likes to be as “specific” as possible, and generics are less specific. Functions with non-generic arguments (even ones that are protocols) are always preferred over generic ones:

This doens't seem to have any relation to Equatable, though; this test shows us the same behaviour:

class Foo {};
class Bar<T>: Foo {};
class Baz: Bar<Int> {};
class Qux<T>: Baz {};

func test(foo: Foo) {
    print("Foo version!");
};

func test<T>(bar: Bar<T>) {
    print("Bar version!");
};

func test(baz: Baz) {
    print("Baz version!");
};

func test<T>(qux: Qux<T>) {
    print("Qux version!");
};

let foo = Foo();
let bar = Bar<Int>();
let baz = Baz();
let baz2: Bar<Int> = Baz();
let qux = Qux<Float>();

test(foo);  // Foo
test(bar);  // Foo
test(baz);  // Baz
test(baz2); // Foo
test(qux);  // Baz

So what is happening here is that when choosing a free function, along with using its static type instead of the dynamic type, Swift prefers not to use any generics, even if that generic is a type parameter and indeed it should be the most specialized choice.

So, it seems that to solve the issue, as suggested by @VMAtm, you should add a method like equalTo to the class instead, so that the actual method is picked up at runtime.

like image 117
paulotorrens Avatar answered Oct 03 '22 17:10

paulotorrens


Can't say that I'm an expert for the Swift operator overloading, but I found an article you can find some useful information:

For reference types, the equality becomes conflated with identity. It makes sense that two Name structs with the same values would be equal, but two Person objects can have the same name, but be different people.

For Objective-C-compatible object types, the == operator is already provided from the isEqual: method: .... For Swift reference types, equality can be evaluated as an identity check on an ObjectIdentifier constructed with an instance of that type:

Please, also consider this answer for similar problem:

The reason the equality for A is being invoked for an Array that contains B is that overloading of free functions is resolved statically, not dynamically – that is, at compile time based on the type, not at runtime based on the pointed-to value.

This is not surprising given == is not declared inside the class and then overridden in the subclass. This might seem very limiting but honestly, defining polymorphic equality using traditional OO techniques is extremely (and deceptively) difficult. See this link and this paper for more info.

The naïve solution might be to define a dynamically dispatched function in A, then define == to just call that: ... Then when you implement B, you’d override equalTo:

So, may be the reason that your code isn't work that it's being called after statical resolving by compiler, which is being done without knowing that you'll override == operator for some anchestor of Foo.
May be you should do like this (equality logic moved to function being called from operator:

public class Foo: Equatable {
  func equalTo(rhs: Foo) -> Bool {
    // base logic here
  }
}
public func ==(lhs: Foo, rhs: Foo) -> Bool {
  // check for type here and call appropriate function]
  // may be this will be done automatically as Bar overloads equalTo function
  return lhs.equalTo(rhs)
}

public class Bar<T: Equatable>: Foo {
  public var bar: T?
  public init(barIn: T?) {
    self.bar = barIn
  }
  override func equalTo(rhs: Foo) {
    // cast rhs to Foo here
    // if it can't be done, return false
    return (rhs as? Foo).map { foo in
      return self.bar == foo.bar
    } ?? false
  }
}
like image 21
VMAtm Avatar answered Oct 03 '22 18:10

VMAtm