Function with two underscore parameters and a default — should this compile?

Given the following:

func foo(_ first: String = "Hello", _ second: String) { 
    print("\(first), \(second)")
}

foo("World")

I would expect that the compiler would understand that my intent is to keep the default parameter for _ first ("Hello") and that I'm passing "World" as _ second and print Hello, World.

Instead, this fails to compile with the error:

Missing argument for parameter #2 in call

Is this a bug that should/could be fixed?

It is not a “bug” because it is the intended design: call-site arguments are assigned greedily to the next function parameter with a matching label. Here are a couple of older threads about the topic:

Rationale for Swift’s overload resolution?
Ordering Of Unnamed Parameters & Parameters With Default Values

It would technically be possible to change the design. There exists an algorithm that can match “as greedily as possible” while allowing defaulted parameters to be skipped if necessary. The “obvious” ways to do so take O(mn) time, where m is the number of arguments at the call-site and n is the number of parameters in a candidate function. I have heard that a quasilinear algorithm exists as well.

This is related to the longest common subsequence problem. I can’t remember the name of the exact problem off the top of my head. It is essentially a test for “is A (the call-site argument labels) a subsequence of B (the declaration-site argument labels), which contains subsequence C (the non-defaulted labels), plus some fiddly stuff about trailing closures.

It’s a bit more tricky than that though, because the positioning of C within A must be “compatible” with the positioning of C within B.

2 Likes

I'd say it's not a bug it's a feature. Or even this:

func foo(_ first: Int = 42, _ second: String) {
    print("\(first), \(second)")
}

foo("World") // 🛑 Missing argument for parameter #1 in call

Related: there is an API design guideline of 'Prefer to locate parameters with defaults toward the end' with one of the justifications being that it produces a stable initial pattern of use.

2 Likes

Sorry, what do you mean by “or even this”? Is this an example that lives in the same scope to introduce ambiguity to the compiler?

I meant that even if the user intent was obvious and unambiguous in this case we could still consider the current Swift behaviour "a feature".

I’m aware of the guideline, but there are always motivating exceptions. For instance, expressing a REST call with the verb (http method) coming first (and defaulting to ”GET”) where external argument labels are superfluous:

func response(_ method: String = “GET”, _ path: String) async throws -> Response { 
    …
}

let builds = try await response(“/builds”)
let newNote = try await response(“POST”, “/note”)

Thank you for the explanation. This gives me something to think about.

I think that example proves the opposite point. A user can far too easily reverse the method and path arguments and the compiler cannot catch the mistake; it would only be detected at runtime. Thus, the labels aren't actually superfluous.

Even if you replaced the stringly-typed HTTP method argument with an enum, and if you really want to keep the HTTP method first, I don't think adding a from: label to the path argument is onerous at all; it reads more fluently as a result.

1 Like

I see your point. At first it seemed to me like the compiler should be able to infer what to do, but just because it could (if the greedy strategy was foregone) doesn’t mean that it should [insert Jeff Goldblum meme here].

Thanks everyone for sharing your insights and helping me arrive at the same conclusion. Consider this closed!

Perhaps we should introduce a compile-time warning for the situation where two consecutive function parameters have the same label and the first one has a default value while the second does not.

That default value will never be used at any call-site, so a warning at the declaration seems reasonable.

1 Like

Indeed. Or, this?

foo(default, "World") // Hello, World
foo(default + "!!!", "World") // Hello!!!, World