Confused about when I need "override"

In the following snippet, I am trying to understand why swiftc says I need the override keyword for D.bar() but not for D.foo().

class A {
}

class B : A {
}

class C {
    public func foo(x : A) {
    }

    public func bar(x : (A) -> Void) {
    }
}

class D : C {
    public func foo(x : B) {
    }

    // ERROR -- needs override
    public func bar(x : (B) -> Void) {
    }
}

The Swift compiler complains that D.bar() overrides C.bar() so it needs the override keyword. Through experimentation, I think this is because B is a subclass of A, so the function type (B) -> Void is considered the same as (A) -> Void.

But if that's true, shouldn't similar reasoning apply to D.foo()? The compiler accepts this one without override. In fact, if I add the override keyword to D.foo(), it complains.

I'm using Swift 5.4.

Any insight here would be appreciated.

Hi Eric!

This was subtle to think about, but I don't think this diagnostic is wrong. foo(x:) does not override (merely overloads) because if it did override, it would violate the Liskov substitution principle. In the case of bar(x:), I don't see any way to produce an A where only a B is allowed, so the principle does not appear to be violated.

Let's walk through it. bar(x:) doesn't take an A or B, it takes a closure that then takes an A or B. In the context of D.bar(x:), the only valid thing to pass to x is an instance of B. In the context of C.bar(x:), any A can be passed. If somebody upcasts a D to C, then calls bar(x:) on it and passes a closure taking A instead of B, then any B passed to the closure by D.bar(x:) is still a valid argument to that closure. Thus, the principle is not violated.

If you've got a counterexample that violates the LSP, I'd love to see it—it would definitely be worth a compiler bug report!

4 Likes

To add to the above explanation: in FP lingo, the appropriate terms are covariant and contravariance.

The question you need to ask is: can the method in the subclass be used to derive a correct and total implementation of the method in the super class?

In case of foo, this doesn’t work, because not all As are Bs. In case of Bar, it does work, because any function that handles arbitrary As can handle in particular all Bs. In one of the methods, A is in covariant position, in the other one in contravariant position.

3 Likes

Close, but not quite correct:

The type (B) -> Void is a supertype of (A) -> Void.

Anywhere you have an instance of type (A) -> Void, you can upcast it to (B) -> Void and it will work fine. After all, the only thing you can do with a (B) -> Void is call it by passing in an instance of B, and any B is also an A.

More generally, if A: B and U: V, then the type (B) -> U is a supertype of (A) -> V, or equivalently, (A) -> V is a subtype of (B) -> U.

2 Likes

TIL more about covariance than I knew before.

Because this situation involves closures instead of, say, collections, my mind just didn't think of it in terms of coariance or contravariance. The relevant wikipedia page contains some good info on how these concepts play w.r.t. function types:

Below is my original code snippet, with less whitespace this time, and with the signatures of the bar methods "inverted", showing how contravariance for function parameters makes the subtyping go "in the opposite direction".

class A {}
class B : A {}
class C {
    public func foo(x : A) {}
    public func bar(x : (B) -> Void) {}
}
class D : C {
    public func foo(x : B) {}
    public func bar(x : (A) -> Void) {}
}

Thanks for the helpful replies!

2 Likes

Just to be abundantly clear on this, now neither foo nor bar is an override.

You can test this by adding print statements into the function bodies, like print("C.foo") and so forth. Then a function like this:

func foobar(_ c: C) {
  c.foo(x: A())
  c.bar{ _ in }
}

foobar(D())

Will show that the versions from C were used, not the versions from D. (The same thing holds if foobar is generic over T: C, or if it is an extension method on C.)

Yes. Good clarification. Thanks.

Hah! I can never remember which is which so I just avoid saying either. :grinning_face_with_smiling_eyes:

4 Likes