Search code examples
swiftinheritanceinitialization

class AnyComparable inherits from class AnyEquatable


public class AnyEquatable: Equatable {
    public let value: Any
    private let equals: (Any) -> Bool

    public init<E: Equatable>(_ value: E) {
        self.value = value
        self.equals = { $0 as? E == value }
    }

    public static func == (lhs: AnyEquatable, rhs: AnyEquatable) -> Bool {
        lhs.equals(rhs.value) || rhs.equals(lhs.value)
    }
}

public class AnyComparable: AnyEquatable, Comparable {
    private let compares: (Any) -> Bool

    public init<C: Comparable>(_ value: C) {
        super.init(value)
        self.compares = { ($0 as? C).map { $0 < value } ?? false }
    }

    public static func < (lhs: AnyComparable, rhs: AnyComparable) -> Bool {
        lhs.compares(rhs.value) || rhs.compares(lhs.value)
    }
}

I've got an error:

Overridden method 'init' has generic signature C where C: Comparable which is incompatible with base method's generic signature E where E: Equatable; expected generic signature to be C where C: Equatable

Why does the compiler think that I'm overriding initializer? I didn't use the word "override". I can't override here because the method 'init' has different signature (which the compiler tells itself). It's not an overriding. I want the subclass to use its own initilizer, and I (obviously) don't want inherit superclass initializer.
So, how can I achieve that? What can be more natural than something comparable inherits from something equatable! Why can't I do that?


Solution

  • Note that your initialiser in the subclass does override the initialiser in the superclass. If you fix the constraint on C as the compiler asks you to, the next thing the compiler will tell you, is that you are missing an override keyword.

    This behaviour is quite consistent, so it seems like it is deliberately designed that the generic constraints are not taken into account when determining whether one method overrides another. Here's another example of this:

    class A {
        func f<T>(t: T) {}
    }
    
    class B: A {
        func f<C: Equatable>(t: C) {} // error
    }
    

    Anyway, one way you can work around this is to just change the argument label:

    public init<C: Comparable>(comparable value: C) {
        self.compares = { ($0 as? C).map { $0 < value } ?? false }
        super.init(value)
    }
    

    The downside of this is that the client code gets a bit less concise. You have to do AnyComparable(comparable: something) every time, which is quite long. One solution is to put both AnyEquatable and AnyComparable in the same file, make both initialisers fileprivate. You can then provide factory functions (in the same file) that create these classes. For example:

    public extension Equatable {
        func toAnyEquatable() -> AnyEquatable {
            .init(equatable: self)
        }
    }
    
    public extension Comparable {
        func toAnyComparable() -> AnyComparable {
            .init(comparable: self)
        }
    }
    

    Also note that your implementation of < doesn't make much sense. It should be:

    public static func < (lhs: AnyComparable, rhs: AnyComparable) -> Bool {
        lhs.compares(rhs.value) || (rhs != lhs && !rhs.compares(lhs.value))
    }
    

    and compares should mean "less than",

    self.compares = { ($0 as? C).map { value < $0 } ?? false }
    

    to say "lhs < rhs or rhs > lhs".

    But even still, there are cases where using this class with completely unrelated types still doesn't make much sense, just FYI:

    let a = AnyComparable(comparable: "foo")
    let b = AnyComparable(comparable: 1)
    print(a < b) // true
    print(a > b) // true
    print(a <= b) // false
    print(a >= b) // false
    print(a == b) // false
    

    So please do be careful!