[Pitch] Allow default parameter overrides

The title says pitch and people including myself have repeatedly stetted we thing that a change is in order!

Types don't have signatures; methods do. Types do have APIs, and if you guarantee that C1.c = 1 in one version of your library, then that could be something your users rely on going forward. I'm not actually sure if Swift adopts that particular interpretation today, but I don't see why it couldn't acknowledge a difference between C1 and C2 in that respect for resilience purposes.

The pitch says that the behavior (of overriding) should be "corrected" for consistency, suggesting that the author believes it to be inconsistent with the rest of Swift today. Therefore, I explained why it's not. Come now.

I have a couple of different items in my Swift wish list that would cover the feature you are pitching.

Currently you can't use the instance itself to construct the default value of an argument:

class Class {
    
    var defaultFoo: String = "Default Foo"
    
    func f(foo: String = defaultFoo) { // error: cannot use instance member 'defaultFoo' as a default parameter
        print("f(\(foo)")
    }
}

The manual workaround for this is:

class Class {
    
    var defaultFoo: String = "Default Foo"
    
    func f(foo: String? = nil) {
        let foo = foo ?? defaultFoo
        print("f(\(foo)")
    }
}

I wish Swift implemented default arguments automatically like the above manual workaround, so that it would let me directly refer to the instance itself (and even non-defaulted argument values) in the value of the default argument. If Swift acted like that, default parameter overrides would also automatically work.

The other feature I wish Swift had is being able to make default arguments part of protocol contract. For example I wish protocols supported the following syntax:

protocol Protocol {
    
    // Specify the argument should have a default, without specifying it:
    func f(foo: String = default)
    // The above would let me call `f()` on a protocol instance.
    
    func g() -> String
    
    // Specify the argument should have a default and define the default value:
    func h(bar: String = g())
    // Or constant:
    func h2(bar: String = "Constant Default")

}

The above features provide a superset of what you are asking and I would pitch these features instead if I had time to follow it up.

3 Likes

The phrase:

Is from you not be. Are you now backing away from x = 1 as part of the signature :)

Then arguably it could be considered API.

That said, I literally do not know whether it is or is not. Again, this is a factual question: and checking with Xcode, it does not appear to be.

Indeed I alluded to this above. It'd be nice to have and IMO a sensible addition.

TL;DR I agree

This is part of a wider discussion on another thread - which I support.

This would be an easy addition to interpreting default values as a shorthand for multiple method definitions.

These are also part of a larger discussion, method bodies in protocols, and again something I support.

1 Like

How would you do:

and retain the interpretation that a default is part of the signature. Obviously it is easy to interpret this as:

func f()
func f(foo: String)

With the interpretation that a default is a shorthand for multiple functions.

This is a good point. I was thinking about it yesterday.

At the very least we can add support for concrete default values in protocols. Should be something that finds broad agreement, I should think.

This indeed is confusing and more importantly, it's illogical.

This might be the way things work right now in the language. However, if I call a method on MySuperClass and end up calling the overridden method of MySubClass, since originally the object is of type MySubClass, it isn't logical and natural to get the signature of MySuperClass.method triggered (if default values really are part of the signature) considering this,

class A { func foo(_ arg: Int = 5) }

class B: A { func foo(_ arg: Int = 8) }

B().foo() // prints 8

which is IMO intuitive.

As @xwu mentioned, it is a good idea to simply restrict such semantics to avoid confusion, though, in terms of implementation, I do not understand how can it be a 'big hammer' if, for instance, the compiler is OK with tracking the spelling of overridden method signatures.

I am in favor of generally complementing default value semantics this way and moreover, it feels natural. if this implies some core changes it should take a back seat due to the primary focus. On the other hand, if that is the case, we should consider this will become next to impossible after stabilizing the ABI.

Perhaps there's a deeper meaning of the term "signature" that I'm unaware of, but as far as I can tell, in most languages it means "the characteristic fingerprint--including (or depending upon) attributes, name, argument names and types, and return type--that uniquely identifies a specific method or function."

A method overridden by a subclass must, by definition, have the same signature as the method in the parent class, otherwise it's just a separate, additional method and not an "override" in any meaningful sense.

According to this definition, default values are not in fact part of the signature in the current implementation of Swift. If they were, the compiler would complain about the use of override here

override func printStatement( statement : String = "I am the subclass") { ... }

with the message "method does not override any method from its superclass".

The compiler may well squirrel the default value away using the method signature as a key, but that fact in itself does not make the default value "part of the signature".

1 Like

This is a wonderful idea, as well as decent complement to Pitch: Allow functions with default arguments to fulfill protocols

No, parameters can be contravariant and return types can be covariant when a method overrides that of a superclass. Allowing different default values is of-a-kind here.

You can think of it roughly as asking the question, "does the signature of the overriding method require an implementation that works for all possible inputs allowed by the signature of the overridden method?"

1 Like

Ah yes, good point. But are methods with those modifications considered to have identical signatures, or are they simply compatible signatures for purposes of inheritance?

If I define the functions

func setupController(_ ctl: UIViewController) { ... }
func setupController(_ ctl: UITableViewController) { ... }

the compiler has no trouble distinguishing them and allowing them to coexist in the same namespace. That suggests that they do in fact have distinct signatures, despite their obvious type-hierarchy relationship and substitutability.

Except that there's literally no limit on what you can do (from a syntactic perspective) with the default value in an overridden method. Leave it out. Change it. Repeat the original default. The compiler is fine with all of it, though it botches the semantics in the various ways illustrated above.

If there is not even the possibility of incompatibility based on something you can do with default values, how can they be said to be part of the signature?

My goal isn't to derail this thread into a discussion of what "signature" means so much as to point out that yes, there is absolutely ambiguity in the current implementation. To call it broken might be a bit extreme, but there is no lack of surprising behavior, even for those who stop to carefully consider the details.

1 Like

Right, they don't have identical signatures. That's why I'm correcting your statement that a method overridden by a class must have the same signature in the subclass and the parent. They do not; they must have certain relationships that render one suitable to be an override of the other.

The presence or absence of a default value does factor into whether one method can override the other, as shown in the case of protocol requirements. There's no botching by the compiler as all of it behaves in line with first principles that you can reason from. It's just that the rules are hard to reason about, and people become surprised by it. We can improve diagnostics, and we can remove the possibility of this behavior if desired. There are probably other avenues to explore as well, but I don't think fundamentally overhauling argument lists is in the cards.

This is part of a larger discussion:

protocol P {
    func example(x: String) -> String
}
extension P {
    func ext() -> String {
        return "pExt"
    }
}

class A {
    func example(x: String = "aDefault") -> String {
        return "super: " + x
    }
}

class B: A, P {
    override func example(x: String = "bDefault") -> String {
        return "sub: " + x
    }
    func ext() -> String {
        return "bExt"
    }
}

let b: B = B()
b.example() //sub: bDefault
b.example(x: b.ext()) //sub: bExt
let bAsA: A = b
bAsA.example() //sub: aDefault
let bAsP: P = b
bAsP.example(x: bAsP.ext()) //sub: pExt

I agree that this seems like a bug.

I disagree that it should be considered part of the signature. The signature is the function name, external parameter names, parameter types, error handling, and return type. Default values are defined near the signature, but it doesn't make them part of it any more than the internal parameter names are part of the signature (if they were, I couldn't use different internal names when implementing a protocol, for example).

If it actually was part of the signature, then it wouldn't be able to recognize the overload as the same method.

One way or another, it is a bug.

Ok, let's go down a level:

class Foo {
	func bar(defaultArgument: URL = URL(fileURLWithPath: "/Foo/Bar/")) {
		print("XXX")
	}
}

This gets translated into:

__T06Tester3FooC3bary10Foundation3URLV15defaultArgument_tFfA_:
0000000100002350	pushq	%rbp
0000000100002351	movq	%rsp, %rbp
0000000100002354	leaq	0x1465(%rip), %rdi ## literal pool for: "/Foo/Bar/"
000000010000235b	movl	$0x9, %eax
0000000100002360	movl	%eax, %esi
0000000100002362	movl	$0x1, %edx
0000000100002367	callq	0x100002d92 ## symbol stub for: __T0S2SBp21_builtinStringLiteral_Bw17utf8CodeUnitCountBi1_7isASCIItcfC
000000010000236c	movq	%rax, %rdi
000000010000236f	movq	%rdx, %rsi
0000000100002372	movq	%rcx, %rdx
0000000100002375	callq	0x100002d8c ## symbol stub for: __T010Foundation3URLVACSS15fileURLWithPath_tcfC
000000010000237a	popq	%rbp
000000010000237b	retq
000000010000237c	nopl	(%rax)
__T06Tester3FooC3bary10Foundation3URLV15defaultArgument_tF:
0000000100002380	pushq	%rbp
0000000100002381	movq	%rsp, %rbp
0000000100002384	pushq	%r13
   ....

I am personally not sure what should count as signature. Is it the entire code within __T06Tester3FooC3bary10Foundation3URLV15defaultArgument_tFfA_?

Here's a meta observation about this discussion: not distinguishing between is statements and should be statements.

The signature of a method today includes the default value. It's not like an internal parameter name because it's not, well, internal. Specifically, the default value is emitted into the call site.

An override (we're not talking about an overload) does not have to have the same signature as the method being overridden. Covariance of return types and contravariance of parameter types are one example; nor is that analysis limited to formal subtyping relationships. For example, a return type tuple element can be a subtype of the overridden method's, but those two tuple types do not have a subtyping relationship in Swift.

Note that these are all is statements. It's important that we're all on the same page about the facts before we start evaluating how we feel about it.