Why are default arguments not allowed in protocols?

I searched for an answer but only found confirmations that you can't specify default arguments and workarounds.

Can you explain why? I've just run into this myself, and after 20 minutes of searching, I've found workaround, but no answer why. it seems like a perfectly reasonable thing to want to be able to do:

protocol
ToolComponentRepository : Repository
{
    func find(for inVendorID: UUID, page inPage: Int? = nil, pageSize inPageSize: Int = 10) -> EventLoopFuture<[ToolComponent]>
}

Sure, I can make an extension and implement this explicitly, but why create the restriction in the first place? Why have default arguments at all?

7 Likes

Afaics there are no technical reasons for the limitation - but things that would have to be decided, and decisions can be quite complicated here ;-)

What if the implementing type does not include the default value - or even a different one?
Would that still be a valid conformance? What when you use that type as instance of the protocol? Which value should it take?

I think it might be preferable to allow default arguments, but only use them for autocompletion when implementing the requirement.

1 Like

The conforming type doesn't and shouldn't care about the default value. They implement the requirement which accepts the parameter, and whether that value is explicit or a default is none of their business.

So for this example:

protocol
ToolComponentRepository : Repository
{
    func find(for inVendorID: UUID, page inPage: Int? = nil, pageSize inPageSize: Int = 10) -> EventLoopFuture<[ToolComponent]>
}

You would conform as if the defaults weren't there:

struct MyType : ToolComponentRepository
{
    func find(for inVendorID: UUID, page inPage: Int?, pageSize inPageSize: Int) -> EventLoopFuture<[ToolComponent]> {
        // ...
    }
}

This is just like defaults and wrapper functions in protocol extensions. You could make loads of wrapper functions which forward to the requirement but with some default values for particular parameters. The conforming type also doesn't get a say in what values those should be.

1 Like

I agree with @Tino. However I wonder, what is the correct naive approach to understand interaction between protocol declarations and conforming types?

Following image demonstrates such problem from a different angle. I’m not able to conform to a protocol by introducing a function with a default value in a conforming type.

My naive explanation is, that if I introduce function with (one) default parameter, “API” of the type then contains the function and one more function for each combination of default parameters. Therefore the value default parameter is hidden from the protocol and it is reasonable, that protocol can not declare function with default parameter.

But in practise as demonstrated above, there is a difference, which suggests, that the protocol to a degree knows, how the function was declared in a conforming type. Which suggests for me, that if there would be a possibility to declare a function with default parameters in a protocol, conforming type would have to declare precisely the same function.

I think, that this ambiguity for a naive swift user could be solved by and explanatory footnote in the Swift Guide, which would describe how the call is synthesized by the compiler. (Instead of changes to the language itself.)

All these arguments seem to look at it from the conforming types's view. What about from a client's view? A client would like to call a method in a protocol specifying only the desired arguments, just like one might on a method in a struct. The caller doesn't much care about the conforming implementation, it only cares about how much it has to provide.

It's the inconsistency that bothers me. And the workaround, to implement a wrapper method in an extension, can be quite confusing (why doesn't it end up calling itself recursively? I know it's possible to get it to do so, the compiler warned me. But it's not immediately obvious why).

3 Likes

is simple, but what about

struct MyType: ToolComponentRepository {
  func find(for inVendorID: UUID, page inPage: Int? = 0, pageSize inPageSize: Int) -> EventLoopFuture<[ToolComponent]> {
  // ... 
  } 
}

Should be allowed, shouldn't it? But then, it won't be obvious what value find would actually use when the page parameter is missing.
We would end up with another incarnation of the problem with protocol extensions, where the choice which method is executed depends on the context (is it an instance of the type, or of the protocol?).

To be clear, I don't think that all those questions are show stoppers - there just has to be some consensus on the answers.

Another question is:
What if two protocols declare a method with identical signature, but different default values?
Imho this isn't a situation that would happen in real-world code, but still, a sound language needs a proper answer to such edge cases.

1 Like

All very good points.

I have, though, had occasions in the past where I wished I could specify, in the protocol requirement, that a parameter should have some default value, which is a concern pertaining to the API.

That is distinguishable from the question of what to do if the protocol specifies one particular default value and conforming type specifies another or none.

I think it would work just fine to permit the syntax we already see in interface files and documentation actually to be written as a protocol requirement:

protocol P {
  func foo(_ argument: Int = default)
  // A conforming method must default this value to something.
}
17 Likes

I think this would be fantastic. Currently, defaults can sometimes be emulated with overloads but that is boilerplate heavy and often not a good idea.

2 Likes

Note that default arguments are determined statically. Hence this behavior:

class Shape {
  func draw(colour: String = "Red") {
    print("\(colour) shape")
  }
}

class Circle: Shape {
  override func draw(colour: String = "Blue") {
    print("\(colour) circle")
  }
}

let s1 = Shape()
s1.draw() // Red shape

let s2 = Circle()
s2.draw() // Blue circle

let s3: Shape = Circle()
s3.draw() // Red circle

Whereas this "you must provide a default" protocol behavior would need to be determined dynamically (say if you had a Shape existential). Which is not to say it's ruled out, but it would introduce an inconsistency in the language.

12 Likes

I'd be fine with this static behavior of protocols. I guess I don't see the conceptual difference between your Shape & Circle example and this. That is to say, if the class inheritance example's behavior is acceptable, so should be similar behavior of protocol conformance

protocol Shape {
    func draw(colour: String = "Red")
}

struct Circle {
    func draw(colour: String = "Blue") { ... }
}

let s1: Shape = Circle()
s1.draw() // Red circle

let s2 = Circle()
s2.draw() // Blue circle
2 Likes

I was speaking to @xwu's proposal of func foo(_ argument: Int = default) specifically. That is what mandates runtime defaults because the protocol does not provide a value.

3 Likes

Fantastic idea! There ought to be expressions to declare default parameters, which make a difference to the callers. And this syntax is clear and expressive.

Appreciate this thread is a year old now but are there any plans/discussions around allowing this in protocol definitions?

I come across this regularly and it always feels like a weird and confusing language hack to use a protocol extension that calls through to the concrete implementation.

1 Like

This is how you can do this now:

protocol A {
    func myFunc(arg: Int)
}
extension A {
    func myFunc(arg: Int = 1) {
        myFunc(arg: arg)
    }
}

But you may make a bug, for example, somone changes signature:

protocol A {
    func myFunction(argument: Int)
}
extension A {
    // this will cause stack overflow:
    func myFunc(arg: Int = 1) {
        myFunc(arg: arg)
    }
}

This is how to overcome stack overflow:

protocol AInterfaceToImplement {
    func myFunction(argument: Int)
}
protocol A: AInterfaceToImplement {}

extension A {
    func myFunc(arg: Int = 1) {
        (self as AInterfaceToImplement).myFunc(arg: arg) // compile time error
    }
}

But my colleagues protest against my pull requests with such code, so I don't use it.

If you use the @_implements(A, myFunc(arg:)) directive to make an implementation (with a different name) that implements myFunc(arg:) does it still crash if A’s definition changes?

enumerating all possibilities:

protocol Foo {
	func foo_no_default_value(param: Int)
	func foo_explicit_defaut_value(param: Int = 123) // hypothetical new syntax
	func foo_some_defaut_value(param: Int = default) // hypothetical new syntax
}

struct Bar: Foo {
	// func foo_no_default_value(param: Int) {} // obviously ok
	// func foo_no_default_value(param: Int = 123) {} // feels a bit wrong. thoughts?

	// func foo_explicit_defaut_value(param: Int = 123) {} // obviously ok
	// func foo_explicit_defaut_value(param: Int = 456) {} // hmm. feels wrong!
	// func foo_explicit_defaut_value(param: Int) {} // very wrong!

	// func foo_some_defaut_value(param: Int = 123) // obviously ok
	// func foo_some_defaut_value(param: Int) // very wrong!
}

the proper fix (not in swift currently).

For a single-argument case like this, here's a safer solution that won't stack overflow if the protocol requirement is removed:

protocol A {
    func myFunc(arg: Int)
}

extension A {
    func myFunc() {
        myFunc(arg: 1)
    }
}

How does this work? Isn’t this arg an undefined symbol?

Oops, ha ha. I've fixed it.

1 Like
Terms of Service

Privacy Policy

Cookie Policy