[Pitch] Allow default parameter overrides

Currently when a super class and a subclass both have default parameters for a function (including init ) the subclass overrides the logic, but not the default values of the function.

class MySuperClass {
    func printStatement( statement : String = "I am the superclass") {
        print( "superclass: \(statment)")
    }
}

class MySubClass : mySuperClass {
    override func printStatement( statement : String = "I am the subclass") {
        print( "subclass: \(statement)")
    }
}

Considering the classes defined above the following code

let classInstance : MySuperClass = MySubClass.init()
classInstance.printStatement()

produces: “subclass: I am the superclass”

This is not the behavior that a developer would expect to find under these conditions and should be corrected to have consistent logic.

In this simple case the solution to the issue would be to move the printStatement function to a protocol and have both classes reference the protocol. However in more complex cases the subclass may need to build on superclass functionality for the function.

Edit Protocols handle overrides in a different way as well

protocol OverridePrintProtocol {
    func printTest(param : String)
}

extension OverridePrintProtocol {
    func printTest(param : String = "Ex Default") {
        print("extension logic: \(param)")
    }
}

class ImplementationClass : OverridePrintProtocol {
    func printTest(param : String = "Class Default") {
        print("Class logic: \(param)")
    }
}

With the above code the following code

let overrideTest : OverridePrintProtocol = ImplementationClass.init() overrideTest.printTest()

produces: “extension logic: Ex Default”

while
overrideTest.printTest(param: "anything")
produces: "Class logic: anything

So here with no argument the extension logic is executed, and with an argument the class logic is executed. Note that it doesn’t matter if the default value is the same in all cases giving the exact same signature. The behavior is the same

3 Likes

This is a tricky problem. I can see how it’s confusing.

However, "I am the superclass" is part of the signature of mySuperClass.printStatement and not just its implementation. Overrides shouldn’t change signatures. It stands to reason that when I call the function without an argument on a variable of type mySuperClass, I should get the default I see in the signature.

The consistent solution that would prevent confusion would be to make it an error to supply a different default in the subclass, but that seems like a big hammer.

1 Like

The fact that its the signature makes sense, but to keep consistency the body shouldn’t execute as part of the override either. It should either be a new function completely or a full override. The current implementation does half and half.

2 Likes

In most languages (I assume it works the same in Swift) default function arguments are only considered at compile time. Basically the compiler emits code at the call site as if you had written that constant for that argument. That has a few implications:

  1. The compiler has to use the compile-time type to determine which constant to use. It may not know what type will actually be used at runtime so it can’t use that information to choose the constant. This is what you noticed.
  2. If the default argument is part of an API that is exposed to other clients via a binary (like a shared library/framework) then changing the default argument’s value will not affect clients that don’t recompile.

Both of those are direct side effects of the way that default arguments get compiled. If you tried to fix that then it would make default arguments more complex and probably less efficient.

2 Likes

Adam and Xiaodi have it correct, and it’s always possible to get the effect you want by adding an overridable method to mySuperClass (something like ‘defaultStatement()’).

Thats true, having that or an override var would work (as would defining the value in each body if the argument is nil), but doesn’t then the issue become code readability?
Having a separate piece of code that is only used within a single function for a single purpose seems like a burden and poor design.

I would say the ‘best’ behaviour is that it is identical to:

class SuperClass {
    func printStatement() {
        printStatement(statement: "I am the superclass")
    }
    
    func printStatement(statement: String) {
        print("superclass: \(statement)")
    }
}

class SubClass: SuperClass {
    override func printStatement() {
        printStatement(statement: "I am the subclass")
    }
    
    override func printStatement(statement: String) {
        print("subclass: \(statement)")
    }
}

let instance: SuperClass = SubClass()
instance.printStatement()

Which prints:

subclass: I am the subclass

It is confusing for two ways of doing the same thing to give different results.

again, this does solve this specific issue, but its overly cumbersome to need to break every single function into two separate functions that are interdependent. Actually only two if there is a single default variable. In reality you would need one per each variable per function. That gets very cumbersome very fast

Sorry if my post wasn’t clear. I was not offering you a solution to your problem. I was commenting on what I thought Swift should do.

I am not saying you should code it this way; I am not telling you in any respect what to do, that’s up to you.

What I am saying, is that I think Swift’s default values should behave as though the example had been coded the way I showed. In particular I don’t think there should be a change in behaviour between the two coding styles.

1 Like

That behavior is inconsistent with default values being part of the signature, which is the difference between a default value and what you’ve written.

I think that the current behaviour of default values is the one that should be changed. I think the behaviour I have shown is what people would expect - as witnessed by this thread. I also think that you should be able to use the two coding styles interchangeably - having two very similar features that behave differently is asking for trouble.

1 Like

As a user of other languages the behavior I expect is the current behavior. I would be very surprised if it behaved any other way.

1 Like

The current behavior looks like a bug to me.

In Swift, a subclass’s override of a member function is called even when the static type of the variable is the superclass. Therefore, when the runtime type of a variable is the subclass, the subclass’s implementation of the override is the one that must be called.

Since the subclass implementation provides its own default value, that is what should be used.

2 Likes

Another advantage I have just thought of is if a define a default func in a protocol, e.g.:

protocol DefaultPrintStatement {
    func printStatement()
}

Then try and get a class to conform to it using the current behaviour, e.g. extending the original class (mySuperClass - note my) with default print then I get an error:

extension mySuperClass: DefaultPrintStatement {} // This is an error.

If however I extend a class that behaves the way I suggest then it is OK:

extension SuperClass: DefaultPrintStatement {} // Not an error

Setting aside questions of right or wrong, is there an actual (i.e. plausible) use-case for the current behavior?

Its not how all languages behave, e.g. Scala does what I suggest and the feature is liked and used.

Yes, this exists elsewhere, but there I can’t possibly think of any use cases for this type of behavior

Yes, subclasses can override implementations, but they cannot change signatures. In Swift, a default value isn’t strictly a part of the implementation. (Likewise, in Swift, variables bound in an guard condition aren’t in the same scope as the stuff inside the braces, etc.)

When I look at a method defined in the superclass, every part of the signature should apply when I call it.

If this turns out to be confusing, as I said before, the only consistent rule would be to ban contradictory default values in subclasses, but that’s a big change.

Protocols handle overrides in a different way as well

protocol OverridePrintProtocol {
    func printTest(param : String)
}

extension OverridePrintProtocol {
    func printTest(param : String = "Ex Default") {
        print("extension logic: \(param)")
    }
}

class ImplementationClass : OverridePrintProtocol {
    func printTest(param : String = "Class Default") {
        print("Class logic: \(param)")
    }
}

With the above code the following code

let overrideTest : OverridePrintProtocol = ImplementationClass.init() overrideTest.printTest()

produces: “extension logic: Ex Default”

while

overrideTest.printTest(param: "anything")

produces: "Class logic: anything

So here with no argument the extension logic is executed, and with an argument the class logic is executed. Note that it doesn’t matter if the default value is the same in all cases giving the exact same signature. The behavior is the same

That is the part that looks like a bug to me.

When I look at a method overridden in a subclass, every part of that override should apply when I call it.

Please let’s refrain from hyperbolic states like “the only consistent rule would be X”. Clearly there is at least one other consistent rule, namely to have overrides actually, you know, *override* the superclass version.

3 Likes
Terms of Service

Privacy Policy

Cookie Policy