Different behavior using default values with `extension` and `struct`

:wave: Introduction

Method defined on Extension seems to override the default method when default values are used.

:arrow_down: Example


import Foundation

protocol X {
    func say(sentence: String)
}

extension X {
    func say(sentence: String = "Hello Jessy") {
        print("X : \(sentence)")
    }
}

struct A: X {
    func say(sentence: String = "I'm fine, you ?") {
        print("A : \(sentence)")
    }
}

let speaker: X = A()

speaker.say(sentence: "Hello World") // Should print -> A: Hello World
speaker.say()  // Should print -> A: I'm fine, you ?

:calling: Debug Console

A : Hello World
X : Hello Jessy

As you can see above, the implementation method called when default values are used is the implementation defined into the extension rather than the one defined on the struct.

This behavior is weird, implementation should be overridden by the struct.

:wastebasket: Let's try without default values

protocol X {
    func say(sentence: String)
}

extension X {
    func say(sentence: String) {
        print("X : \(sentence)")
    }
}

struct A: X {
    func say(sentence: String) {
        print("A : \(sentence)")
    }
}

let speaker: X = A()

speaker.say(sentence: "Hello World")
speaker.say(sentence: "I'm fine, you ?")
A : Hello World
A : I'm fine, you ?

In this case, speaker call the right initialization defined into the A struct.

The question is: Is this a bug or a feature ?

:heart_eyes: What should be expected


import Foundation

protocol X {
    func say(sentence: String)
}

extension X {
    func say(sentence: String = "Hello Jessy") {
        print("X : \(sentence)")
    }
}

struct A: X {
    func say(sentence: String = "I'm fine, you ?") {
        print("A : \(sentence)")
    }
}

let speaker: X = A()

speaker.say(sentence: "Hello World") // Should print -> A: Hello World
speaker.say()  // Should print -> A: I'm fine, you ?

:calling: Debug Console

A : Hello World
X : I'm fine, you ?

:broom: Improvement

We need to bring some coherence between the behavior with/without default values.

Since the method is declared into the protocol, the struct instance must call its own method and not the one defined into the extension.

On our first example speaker.say() should print A: I'm fine, you ? instead of Hello Jessy.

That's not what I would expect. This is a known trap called 'static dispatch'. Protocols are not allowed to require default values for method or subscript parameters. Therefore if you cast A to X and call say() it's statically dispatched to the protocol extension as the protocol itself it not known to have a customization point for say(). Even if it did, right now a protocol conformance is not satisfied by an overload that theoretically covers both cases.

That said I think the issue that is worth solving here would be this:

protocol X {
  func say(sentence: String)
  func say()
}

struct A: X {
  // error: Type 'A' does not conform to protocol 'X'
  func say(sentence: String = "I'm fine, you ?") {
    print("A : \(sentence)")
  }
}

Then if I'm not mistaken you'll get dynamic dispatch back for in your example.

1 Like

I think it would be awful to solve the problem by making this work. If we want protocols to be able to require conforming types to provide a default argument for a parameter we should do that directly. Here’s an example with strawman syntax:

protocol X { 
    func say(sentence: String = default) 
}

The difference isn’t that significant with one default but you have a combinatorial problem if you want to provide defaults for more parameters. This approach is also more clear and direct.

3 Likes

I think we should make it works without a workaround, what do you think ?

The @anandabits proposal is pretty effective and readable.

Feel free to push this forward. @anandabits would be definitely superior to my solution as it covers all possible combinations you'd need to provide otherwise.

I'd be happier just being able to define default implementations right there in the protocol declaration instead of requiring them to be done in a protocol extension, but then that raises the specter of multiple inheritance. But if you could do this, then defining a default parameter value would be completely natural.

If we allowed concrete default values in a protocol declaration we would have to require all conforming types to specify the exact same default value. That may be desirable in some cases but it is probably not desirable in many other cases. It would also violate the principle that protocol requirements are abstract.

1 Like

Not if we allowed protocols to provide default function implementations.

As Adrian noted, it isn’t really about default values, but is a simple matter of static vs dynamic dispatch (for requirements vs extensions, respectively).

Regardless of any possible future sugar, the proper way to do it now is to refactor to make the default value a protocol requirement so that it dispatches dynamically:

protocol X {
    static var defaultSentence: String { get }
    func say(sentence: String)
}

extension X {
    static var defaultSentence: String {
        return "Hello Jessy"
    }
    func say() {
        print("X : \(Self.defaultSentence)")
    }
}

struct A: X {
    static var defaultSentence: String {
        return "I'm fine, you ?"
    }
    func say(sentence: String) {
        print("A : \(sentence)")
    }
}

let speaker: X = A()

speaker.say(sentence: "Hello World") // A: Hello World
speaker.say()  // X: I'm fine, you ?

// (You can also make `say()` a requirement so that you can override it too
//  and make the last line start with “A: ”.)
2 Likes