4

Bit academic, but look at this code

// Some protocol
protocol R : Equatable
{
}

// Some class
class E : R
{
    // Default implementation
    static func == ( lhs : E, rhs : E ) -> Bool
    {
        print ( "Comparing E ===" )
        return lhs === rhs
    }
}

// Some class with a string
class F : E
{
    let f : String

    init ( f : String )
    {
        self.f = f
    }

    // Copare strings
    static func == ( lhs : F, rhs : F ) -> Bool
    {
        print ( "Comparing F ==" )
        return lhs.f == rhs.f
    }
}

// Some generic container type
class G < T : R > : R
{
    let g : T

    init ( g : T )
    {
        self.g = g
    }

    // Compare
    static func == ( lhs : G, rhs : G ) -> Bool
    {
        print ( "Comparing G ==" )
        return lhs.g == rhs.g
    }
}

let f1 = F ( f : "abc" )
let f2 = F ( f : "abc" )

print ( "f1 == f2 ? \( f1 == f2 ) (expect true)" )

let g1 = G < F > ( g : f1 )
let g2 = G < F > ( g : f2 )

print ( "g1 == g2 ? \( g1 == g2 ) (expect true)" )

This gives

Comparing F ==
f1 == f2 ? true (expect true)
Comparing G ==
Comparing E ===
g1 == g2 ? false (expect true)

The code defines some protocol and then a class E that implements it and provides a generic comparator. Next comes F which overrides this comparator to compare the string f it houses. Finally some container class G that houses something that implements the protocol.

So far so good. Now let us compare stuff. Comparing to instances of F works as the correct comparator is used. However, comparing two container classes does not work and it seems Swift calls too shallow. It calls the generic implementation provided in E in stead of the deeper one overridden in F. If this was Objective-C it would make that deeper call, even if the lhs and rhs were different classes and then it would typically crash. Swift will not allow it, which is easy to see by creating another class that e.g. contains Int in stead of String but at the same time it is not using the correct comparator it seems.

EDIT

This makes it worse. The change below is that f1, f2 and g1 and g2 are defined more generally as being (or containing) superclass types E. Even though F overrides the comparator it is not used in any of the comparisons.

let f1 : E = F ( f : "abc" )
let f2 : E = F ( f : "abc" )

print ( "f1 == f2 ? \( f1 == f2 ) (expect true)" )

let g1 = G < E > ( g : f1 )
let g2 = G < E > ( g : f2 )

print ( "g1 == g2 ? \( g1 == g2 ) (expect true)" )

I suspect this has something to do with operator overloading vs. function overriding ... but any light will be appreciated.

EDIT 2

Toyed a bit with some thinking if I use it then maybe it could get this to work the way I want but some is not allowed for a function argument at present.

skaak
  • 2,988
  • 1
  • 8
  • 16
  • Take a look at https://bugs.swift.org/browse/SR-1729, it looks like it may be related. – msbit Mar 20 '21 at 10:35
  • Possibly related: https://stackoverflow.com/q/39909805/1187415. – Martin R Mar 20 '21 at 10:36
  • @msbit looks like bug and best way around is to implement own comparator and avoid overloaded operators because this kind of a think can poison your code badly – skaak Mar 20 '21 at 10:47
  • @MartinR just tried by moving the comparator to global scope as one suggestion in that post but it remains the same – skaak Mar 20 '21 at 10:48
  • @skaak annoyingly, yes, I agree. The resolution path is very surprising if you've come from other OOP languages. – msbit Mar 20 '21 at 10:58

2 Answers2

2

I've taken a look at the compiled output of a streamlined version of the above:

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

class F: E {
  static func ==(lhs: F, rhs: F) -> Bool { return false }
}

class G <T: Equatable>: Equatable {
  let g: T

  init(_ g: T) {
    self.g = g
  }

  static func ==(lhs: G, rhs: G) -> Bool {
    return lhs.g == rhs.g
  }
}

and the missing link is the absence of a protocol witness for the inheriting class. The use of a protocol witness is detailed here.

If you take a look at the symbol table:

$ nm main | swift demangle | grep 'protocol witness'
00000001000024a0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.E : Swift.Equatable in main
00000001000028e0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.G<A> : Swift.Equatable in main

you can see one for E and G, but not for F.

If you modify the code to add conformance to Equatable, you unsurprisingly get the additional protocol witness:

$ nm main | swift demangle | grep 'protocol witness'
00000001000024a0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.E : Swift.Equatable in main
00000001000025a0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.F : Swift.Equatable in main
00000001000028d0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.G<A> : Swift.Equatable in main

So, yeah it could be very strongly upheld that this is a bug, but as noted in a related bug report it's the way things are, so not likely to be changed without significant ceremony and process.

msbit
  • 4,152
  • 2
  • 9
  • 22
  • Wow thanks - thanks for the effort and insight. I *think* this (OP) example is fairly standard, something you may easily do in typical code, so it is scary, especially since it is related to the Equatable protocol. Operator overloading should be done correctly or not at all and I prefer the latter. – skaak Mar 20 '21 at 10:59
  • The way I read your answer is that it is a bug probably related to optimisation? – skaak Mar 20 '21 at 11:02
  • @skaak thinking about it, I suppose the "swifty" way of doing it would be to specify conformance everywhere directly, as opposed to relying on inheritance. I've heard it said that Swift is more a protocol oriented programming paradigm than OOP, so I guess that fits. – msbit Mar 20 '21 at 11:02
  • @skaak not optimisation per se; more a choice of limiting the role of inheritance. A comment on that linked bug has some context: https://bugs.swift.org/browse/SR-1729?focusedCommentId=21900&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-21900 – msbit Mar 20 '21 at 11:04
  • Trouble is that the same approach is not used for 'normal' overridden functions. I tested and (was not surprised to) find that if you use normal functions it works as expected - then you can rely on inheritance and need not specify conformance. – skaak Mar 20 '21 at 11:06
  • @skaak I'm assuming that basically protocols and classes are treated differently, so if you had an inheritance graph of only classes it would work as traditionally expected, but if one of those methods was a protocol conformance, it would act like this. I may take a look. – msbit Mar 20 '21 at 11:08
  • @skaak just a final note before this gets kicked off to chat, I've taken a look at the the same setup with a general function (ie not an operator) and it acts the way you'd expect, calling the intermediate class' implementation of the function. So, it's probably additionally tricky handling of the operator functions. I realised that one way to deal with it is to specialise the declaration of `G` in your example to `class G : Equatable`, so it "knows" it is dealing with `F`, not any general `Equatable`, for what that's worth. – msbit Mar 20 '21 at 11:23
2

@msbit provided a great explanation of what happens under the hood, I'll try to tackle the problem from another angle.

Let's take a look at the protocol declaration:

public protocol Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool
}

As soon as a protocol uses Self in its declaration, or has associated types, it leaves the dynamic dispatch world, and lands in the static dispatch one. And this static dispatch gets into the way of dynamic method resolution via inheritance.

Another aspect is that the == method is a static requirement, and when it comes to classes, static members receive the static dispatch, instead of the dynamic one. This means that when referencing the derived class though a reference to the base class, the method call will always be dispatched to the base class.

You would need to use class func in order to get to the dynamic dispatch, however protocol declaration don't allow specifying requirements as class func, even if the protocol is a class type one.

Thus, the runtime behaviour is the correct one in this case, even is not the expected one, as it follows the language design. The problem I see is that the compiler allows you to re-implement without overriding a static method (== in this case), as this is not allowed for regular, non-operator, static methods.

class Base {
    static func sayMyName() {
        print("My name is Base")
    }

    static func ==(lhs: Base, rhs: Base) -> Bool {
        true
    }

    static func >>(lhs: Base, rhs: Base) -> Bool {
        true
    }
}

class Derived: Base {
    // this not allowed by the compiler
    static func sayMyName() {
        print("My name is Derived")
    }

    // this override is allowed, though `Base` doesn't conform
    // to Equatable
    static func ==(lhs: Derived, rhs: Derived) -> Bool {
        true
    }

    // Other operator-like static overrides are also allowed
    static func >>(lhs: Derived, rhs: Derived) -> Bool {
        true
    }
}
Cristik
  • 30,989
  • 25
  • 91
  • 127
  • Thanks this clarifies a lot. I agree with you that this is problematic. ```Equatable``` probably proliferates throughout the libraries and if you have anything more complex than a WWDC demo this will poison your code. @msbit explained why this is a bug and you now explain why this is NOT a bug, but then at least it should be a compiler error in safety-comes-first Swift. To make it an error is probably easier to do for the Swift people, but it also need a bit of thought about the impact when comparing in collections and maybe the right thing to do in the long run is to allow the override. – skaak Mar 20 '21 at 14:41
  • This is a much better explanation of the why than my answer; mine covered mainly the symptoms and not the cause :) Out of curiosity, do you have a link to the relevant parts of the specification for this? – msbit Mar 21 '21 at 00:41
  • @msbit the `static`/`class` thing is part of the [official documentation](https://docs.swift.org/swift-book/LanguageGuide/Methods.html#ID241) describe it, however I could not find explicit links for the static dispatch of protocols with self requirement, though I know the information is at least on the Swift forum, where I originally found out out about it. – Cristik Mar 21 '21 at 06:45