Require parameter names when referencing to functions

Hi everyone,

Some of you may remember SE-0220, my proposal that added the count(where:) function to sequences. This proposal was ultimately accepted in time for Swift 5, but sadly had to be reverted because it was causing issues with the type checker.

The issue was that when you reference count, in an expression like myArray.count * 5, you could be referring to the count property, with type Int, or the count(where:) function, which has type ((Element) -> Bool) -> Int, which you can refer to in shorthand as count. When Swift tries to resolve what version of the * function to use, it has to run through a lot more potential implementations, which explode combinatorially as you increase the number of operators in the expression.

I've thought about this problem for a while and chatted with a few folks about the issue, and I think the simplest path forward to solve this is to require parameter names when referencing functions.

In other words, you wouldn't be able to refer to functions with their shorthand anymore (i.e., count), you'd have to refer to them by their full name (i.e., count(where:)).

This change has the added benefit of making Swift a more precise language as well. There are currently other method resolution problems with situations like myArray.map(Int.init), where the compiler has to do a lot of work to determine which implementation to use. In addition, Int.init isn't particularly specific, and therefore when new initializers get added, the meaning of this code can change or become ambiguous — which has actually already happened — causing an unintended source breakage. Requiring a more complete name would help in these situations as well.

If people like this solution, I'm happy to write up the proposal, and perhaps me and someone else can implement it together?

66 Likes

What's the impact to existing code if this requirement was added?
The compiler pops up some warnings or errors for those functions don't use full name?

Could we revert it back once we can (theoretically) improve type-checker to the point that this doesn't pose a problem?

1 Like

I was imagining we would deprecate "the old way" for a single Swift version (~a year) and then get rid of it, but I imagine people that are more involved and those that work at Apple have stronger feelings about that exact process.

We could, and it would be easier, since it wouldn't require any deprecations. But I also think that this is a better state for Swift to be in in the long term, too, because it's more explicit.

5 Likes

We should make this change and we should not undo it later. This misfeature causes source compatibility breaks left and right, complicates type checking, and makes code counterproductively vague. It's a vector for users to accidentally use underscored initializers and unsafe APIs. And it runs counter to the direction of the language towards treating parameter labels as part of a function's name. Kill it with fire.

(Specifically, name without parentheses should only ever match nullary (zero-parameter) functions/methods/initializers called name. If they have any parameters, you should need to specify the parameter labels.)

Edit: It's worth noting that we can deprecate this kind of matching in any release, but we can only make it an error in a release with a new language version. (For example, Swift 5.1 promises full source compatibility with 5.0, so in 5.1 we could have added this as a warning but not as an error.)

61 Likes

A fix for these kinds of problems would be great, but I don't know how possible it is due to compatibility constraints. One other idea in this general area that has been discussed in the past is to have a keypath syntax for methods, which theoretically could replace the current problematic syntax and give you a nicer source compatibility story.

Yes please! Could this also fix the spurious “value used in its own initial value” errors? e.g. let expectation = expectation(“this doesn’t compile”)

32 Likes

I’m all for this, certainly for count(where:), and for all the reasons Brent mentioned.

How should it handle no-args functions? In other word, consider:

struct Foo {
  func noArgsFunc() { }
}
let foo = Foo()

…how should we get a reference to noArgsFunc? The obvious answer is to keep the current precedent:

foo.noArgsFunc

…but the non-uniformity is troubling:

foo.noArgsFunc                 // no parens
foo.oneArgFunc(x:)             // parens
foo.unlabeledArgsFunc(_:_:_:)  // parens

Following the pattern of other function refs, the syntax should be foo.noArgsFunc() … which is of course indistinguishable from a function call.

I think the approach above is probably good enough despite the inconsistency, but I thought I’d throw it out there.

Addendum: do the problems Brent mentions about source compatibility breaks etc. not apply to no-args / nullary funcs? If not, why not?

9 Likes

This is one area where a keypath syntax for methods could help out a lot.

1 Like

If it gets us count(where:), I want it yesterday.

5 Likes

It will be very annoying to write .init(_:) instead of .init. Could we require writing the full name if there is at least one named parameter, or when the parameters aren't known from context?
It would solve the problems with count and myArray.map(Int.init) being ambigous, while not not forcing you to write the full name in most cases.

func foo() -> Int { return 0 }
func foo(_ a: String) -> Int { return 0 }
func foo(namedParam: String) -> Int { return 0 }
func foo(_ a: String, b: String) -> Int { return 0 }

let a = foo // error: ambiguous, can be first or second
let b: () -> Int = foo // Good, clearly it's the first one
let c: (String) -> Int = foo // Good, there are two that match the signature, but only second has no named parameters
let d: (String) -> Int = foo(namedParam:) // we have to write full name if we want to use the third function
let e: (String, String) -> Int = foo(_:b:) // we have to write full name even if only some parameters are named
10 Likes

This proposal gets a +1 from me. In addition to improving typechecker performance, this change will make Swift code more explicit and legible. Thank you @soroush!

6 Likes

I am leery of unintended consequences from this change.

The intentions behind it are good, but so were the intentions behind SE–111 “Remove type system significance of function argument labels”. That proposal was part of Swift 3, and the proposal text made no mention of the fact that it would outright prohibit parameters of function type from being called with argument labels.

It has been more than 3 years since that change was accepted. The problem was identified almost immediately thereafter, the core team quickly laid out a plan for restoring the lost functionality, and yet we still cannot provide argument labels to parameters of function type.

I do not want to see another regression in the convenience and usability of the language.

12 Likes

You raise a valid concern, but I do think the requirement that proposals must have an implementation before being put up for review would mitigate this risk. Any issues like this should discovered during the review and would hopefully be required to be fixed or mitigated before full acceptance.

2 Likes

I'm not sure it'd help in the place where it's really needed.
+ operators for example is leered upon with +(_:, _:) or +(lhs:,rhs:) with which this rule dosn't help.

It also hurts where there's little-to-no ambiguity like when the function name is unique.

3 Likes

Is there a best of both worlds situation where we can add specificity when needed for disambiguation (emit a warning when there is an ambiguity) yet allow the status quo when it is already 100% clear?

4 Likes

Does autocomplete favor the named version or non named?

First, those would be +(_:_:) or +(lhs:rhs), without any comma in between. Second, just judging by the name of this thread, the first variant wouldn't apply, because that function does not have parameter names, therefore + should be fine. Third, I think we can keep operator functions as is, because full parameter names does not give the compiler any new information about which overload it should pick. To solve that particular area we need someone implement the parsing part to allow explicitly refer to static operator functions like Int.+ or String.+.

1 Like

Huh, It seems I used too much autocomplete :stuck_out_tongue:.

That's exactly my point.

My other point still stands that it'd hurt when there's no ambiguity, esp. for those with long function names.

1 Like