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.
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!
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.
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.
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) {}
}
Just to be abundantly clear on this, now neitherfoo 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.)