8

Swift protocols can provide default implementations for functions and computed properties by adding extensions to them. I've done that plenty of times. It is my understanding that the default implementation is only used as a "fallback": It's executed when a type conforms to the protocol but doesn't provide its own implementation.

At least that's how I read The Swift Programming Language guide:

If a conforming type provides its own implementation of a required method or property, that implementation will be used instead of the one provided by the extension.

Now I ran into a situation where my custom type that implements a certain protocol does provide an implementation for a particular function but it's not executed — the implementation defined in the protocol extension is executed instead.


As an example, I define a protocol Movable that has a function move(to:) and an extension that provides a default implementation for this function:

protocol Movable {

    func move(to point: CGPoint)

}

extension Movable {

    func move(to point: CGPoint = CGPoint(x: 0, y: 0)) {
        print("Moving to origin: \(point)")
    }

}

Next, I define a Car class that conforms to Movable but provides its own implementation for the move(to:) function:

class Car: Movable {

    func move(to point: CGPoint = CGPoint(x: 0, y: 0)) {
        print("Moving to point: \(point)")
    }

}

Now I create a new Car and downcast it as a Movable:

let castedCar = Car() as Movable

Depending on whether I pass a value for the optional parameter point I observe two distinct behaviors:


  1. When passing a point for the optional parameter

    the Car's implementation is called:

    castedCar.move(to: CGPoint(x: 20, y: 10)) 
    

    Output:

    Moving to point: (20.0, 10.0)


  1. When I invoke the move() function without providing a value for the optional parameter the Car's implementation is ignored and

    the Movable protocol's default implementation is called instead:

    castedCar.move()
    

    Output:

    Moving to origin: (0.0, 0.0)


Why?

Mischa
  • 15,816
  • 8
  • 59
  • 117
  • 3
    i believe the default values are added at compilation time therefore the static type of the variable must be used. – Sulthan Mar 06 '17 at 20:25
  • 1
    You are casting Car to Movable so it will call the Movable method. If you don't cast Car to Movable it will call your Car's move method – Leo Dabus Mar 06 '17 at 20:54
  • @LeoDabus: You are correct that if I don't cast the `Car` to `Movable` I don't run into this problem. However, the reason why the problem occurs when I _do_ perform the cast is the whole point of my question. In an actual protocol-oriented implementation I wouldn't know the actual class of the object I'm dealing with — I'd only know that it conforms to the protocol `Movable`. The line `let castedCar = Car() as Movable` is simply a means of imitating this situation in order to keep the example code clean. – Mischa Mar 07 '17 at 22:43

1 Answers1

13

This is due to the fact that the call

castedCar.move(to: CGPoint(x: 20, y: 10))

is able to be resolved to the protocol requirement func move(to point: CGPoint) – therefore the call will be dynamically dispatched to via the protocol witness table (the mechanism by which protocol-typed values achieve polymorphism), allowing Car's implementation to be called.

However, the call

castedCar.move()

does not match the protocol requirement func move(to point: CGPoint). It therefore won't be dispatched to via the protocol witness table (which only contains method entries for protocol requirements). Instead, as castedCar is typed as Movable, the compiler will have to rely on static dispatch. Therefore the implementation in the protocol extension will be called.

Default parameter values are merely a static feature of functions – only a single overload of the function will actually be emitted by the compiler (one with all the parameters). Attempting to apply a function by excluding one of its parameters which has a default value will trigger the compiler to insert an evaluation of that default parameter value (as it may not be constant), and then insert that value at the call site.

For that reason, functions with default parameter values simply do not play well with dynamic dispatch. You can also get unexpected results with classes overriding methods with default parameter values – see for example this bug report.


One way to get the dynamic dispatch you want for the default parameter value would be to define a static property requirement in your protocol, along with a move() overload in a protocol extension which simply applies move(to:) with it.

protocol Moveable {
    static var defaultMoveToPoint: CGPoint { get }
    func move(to point: CGPoint)
}

extension Moveable {

    static var defaultMoveToPoint: CGPoint {
        return .zero
    }

    // Apply move(to:) with our given defined default. Because defaultMoveToPoint is a 
    // protocol requirement, it can be dynamically dispatched to.
    func move() {
        move(to: type(of: self).defaultMoveToPoint)
    }

    func move(to point: CGPoint) {
        print("Moving to origin: \(point)")
    }
}

class Car: Moveable {

    static let defaultMoveToPoint = CGPoint(x: 1, y: 2)

    func move(to point: CGPoint) {
        print("Moving to point: \(point)")
    }

}

let castedCar: Moveable = Car()
castedCar.move(to: CGPoint(x: 20, y: 10)) // Moving to point: (20.0, 10.0)
castedCar.move() // Moving to point: (1.0, 2.0)

Because defaultMoveToPoint is now a protocol requirement – it can be dynamically dispatched to, thus giving you your desired behaviour.

As an addendum, note that we're calling defaultMoveToPoint on type(of: self) rather than Self. This will give us the dynamic metatype value for the instance, rather than the static metatype value of what the method is called on, ensuring defaultMoveToPoint is dispatched correctly. If however, the static type of whatever move() is called on (with the exception of Moveable itself) is sufficient, you can use Self.

I go into the differences between the dynamic and static metatype values available in protocol extensions in more detail in this Q&A.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • Thank you for this very elaborate answer! I learned quite a lot from it. There should be a side note on this behavior in The Swift Programming Language guide as it's not really trivial, expected behavior (unless you know about some compiler details, namely the static and dynamic dispatching). – Mischa Mar 07 '17 at 22:12
  • So generally speaking **default function parameters don't work with protocols**. They are always statically resolved and thus it's always the protocol _extension_'s implementation that's being called because that's what's "visible" to the compiler at build time. – Mischa Mar 07 '17 at 22:15
  • 2
    @Mischa Happy to help :) Yes, generally speaking, default function parameter values don't work with protocols. If you attempt to apply them by excluding one of the parameters, it will no longer match the protocol requirement, and will therefore lose the dynamic dispatch. Even if there *was* a way for a protocol requirement to express that it must be satisfied by a function with a given default parameter value, the implementation for *the evaluation* of that value will *still* be statically decided by the compiler. – Hamish Mar 07 '17 at 22:27
  • 1
    This would lead to a similar situation to the overriding of methods with default parameter values in classes – the method implementation itself could be dynamically dispatched, but the actual expression for the value of the default function parameter would be determined statically (i.e it would be the one from the extension if called on a protocol-typed instance). – Hamish Mar 07 '17 at 22:27
  • Have you in any of your answers addressed [this comment](https://stackoverflow.com/questions/34601931/is-it-possible-to-satisfy-swift-protocol-and-add-defaulted-arguments#comment79663481_40855083). I'm guessing the closes would be this answer itself right? – mfaani Sep 21 '17 at 19:18
  • @Honey First of all note that that particular configuration: 1. Won't allow conforming types to specify their own default parameter values, which may or may not be an issue 2. More seriously, if the conforming type doesn't provide its own implementation of `sendMessage`, *will* in fact cause an infinite loop. – Hamish Sep 21 '17 at 19:27