Require parameter names when referencing to functions

Yet along a related axis, we don’t allow functions to be called with or without argument labels as desired by a client—they are considered a necessary part of the name of the function for the purpose of calling, so why should referencing be any different? That, to me, is a closer parallel than adding a purely type-based overload versus an “overload” based on argument labels. A library author may falsely believe they are safely adding this overload since it has a different argument label than existing methods, without realizing that that they may break clients which are referencing the function (or even if they are aware of the issue, they have no way to address it without changing the base name).

This is also why I’m not super compelled by the “non-specificity is especially useful in your own code” argument. Why wouldn’t the same non-specificity be useful at the call site? You already break all those usages when you change the argument label, so this change doesn’t seem like it would introduce significantly more code churn than already exists today while API details are still in flux.

4 Likes

For the record, a few people have called this "my" proposal, but @cukr was first to suggest it, and several others had already agreed before I posted.

11 Likes

Not sure what the answer to your question is, but I would personally view this as a feature, not a bug. Trailing closure syntax dropping parameter labels was a mistake IMO.

6 Likes

The answer is no. Trailing closure syntax is part of call syntax, not referencing syntax. That doesn't mean it can't be revisited later, but it's not tied to this discussion.

7 Likes

Thanks for clarifying, that's what I guessed would be the case but wanted to double-check.

With that in mind, I think Jordan's 'middle ground' strategy is acceptable — argument labels have semantic value, but it seems silly to need to write _ (one or more times) when all arguments are unnamed.

1 Like

We do have support for this in code-completion, but it could use some love. Currently it only works on non-generic function types.

3 Likes

This example backfires on itself. Let's consider an example that's very close to yours, but without the "objectionable" omission of parameter keywords:

a.map(String.init(_:))

Here's the corresponding list (culled from your greater list):

init(_ c: Character)
init(_ cocoaString: NSString)
init(_ content: Substring.UnicodeScalarView)
init(_ scalar: Unicode.Scalar)
init(_ substring: __shared Substring)
init(_ unicodeScalars: String.UnicodeScalarView)
init(_ utf16: String.UTF16View)
init(_ utf8: String.UTF8View)
init(_ cocoaString: AnyObject)
init(_ sel: Selector)

You can soak in that list too, and realize that the problem you're pointing out is as much about the inscrutability of type inference as it is about the ambiguity of omitting the labels.

5 Likes

You say that as if it's a settled matter that this is problematic. @jrose suggested that this is a problem, but didn't provide any justification for why it's "harder".

It seems arguable that this is no harder than (say) changing the spelling of a keyword on an existing method. In both cases, the outcome is a compilation error or warning that didn't occur previously. In both cases, the library author may consider whether the upheaval is worth the benefits of the change. But it's not obvious that one change is "harder" than the other.

(This is under the assumption that adding the second method does in fact produce a compilation error.)

1 Like

Your list is incorrect. init(_cocoaString:) and init(_sel:) have labels with leading underscores, not unlabeled parameters; you also missed init?(_ codeUnits: Substring.UTF16View) and init?(_ codeUnits: Substring.UTF8View) from later in the list. That leaves:

init(_ c: Character)
init(_ cocoaString: NSString)
init(_ content: Substring.UnicodeScalarView)
init(_ scalar: Unicode.Scalar)
init(_ substring: __shared Substring)
init(_ unicodeScalars: String.UnicodeScalarView)
init(_ utf16: String.UTF16View)
init(_ utf8: String.UTF8View)
init?(_ codeUnits: Substring.UTF16View)
init?(_ codeUnits: Substring.UTF8View)

This is a list I'm quite comfortable with. Every overload here straightforwardly converts something that's already very string-like to a string. None of these overloads are implementation details or security risks.

11 Likes

That's true, but not the "harder" I was getting at. I probably should have phrased it as "no easier than today" though.

I think people understand and agree that you shouldn't change the name of an existing API, at least not without thinking carefully about it. And people understand and agree that it should be okay to add a new API with a new name. So the murkier areas are

  1. adding a new overload-by-type to an existing API, which should be considered carefully, and which this proposal does not really make any more or less safe

  2. adding a new API that has the same base name as an existing API but different argument labels (ignoring default arguments for the time being)

I am pretty strongly of the opinion that (2) should be as safe as adding a new API that has a different base name, and that's what this proposal addresses.

15 Likes

I'm very much in favor of this. I've run into this issue many times when naming a function succinctly but I have a property that starts with the same name. It's really frustrating and makes me have to compromise my method name or property name. I think the requirement to specify the method fully is more precise as stated and resolves all of these issues. Big +1 from me.

4 Likes

I was initially +1 on this, but reading through the negatives I'm a firm -1. It would be a huge breaking change to existing code, and it ruins elegant syntax like foo.map(fn).

I really wish we could solve problems like let foo = foo(arg), but it seems like that can be tackled by improvements to type resolution, without requiring any further disambiguation from the user (it's already unambiguous).

1 Like

I didn't though about it, but would that rule also apply to operators ?
Would it still be possible to write foo.reduce(0, +)

They don't really seem to suffer from the same ambiguity problem, since you can't use operator symbols as a variables.

1 Like

I'm fine with it. And the author of the method has full control - if they want, they can call it firstWhere instead - nothing in the language precludes it. My understanding is that Swift simply choses to, by convention, express method similarity more explicitly than merely a common string prefix, but with also with an explicit '(' terminator on that prefix.

There already has to be some way to disambiguate e.g. first(where:) and first(after:) - the compiler can't implicitly refer to both as merely first. Disambiguating consistently seems like a readability win to me (and lessens vulnerability to API changes).

(granted this hypothetical first(after:) is fairly esoteric, but it's technically valid, functionally distinct, and cannot be disambiguated based on parameter type, which is (T) -> Bool in both)

That all said, if the compiler could disambiguate based on the presence of the trailing closure - between a single-parameter first(where:) and a zero-parameter first - that'd be an interesting avenue. Though it seems like that could be ambiguous when used in an if statement.

4 Likes

The question is: does this hold true in ~all such cases? It seems like this is relying on an assumption / assumed rule that any 'dangerous' 'overloads' deliberately use a distinct [full] name. Which seems like a good idea irrespective of this thread, since otherwise actual invocations of the method could likewise unwittingly using the dangerous version by accident, but have method authors actually been that careful to date?

3 Likes

It seems to me like we have five options proposed here:

  1. Status quo
  2. Require parens and parameter names for every non-nullary function reference
  3. Require parens and parameter names only when required to disambiguate between a property and a function, or multiple functions with the same base but different parameters
  4. Require parens and parameter names if there is a single parameter labeled with words but not when they have only underscores (so max would be exempt but contains(where:) wouldn't be)
  5. Syntax for providing an alternate name, available only for references but not callers

It seems like both 4 and 5 are workable, with 5 being a little more challenging to implement?

1 Like

Isn't this just the status quo?

Not quite. In the world of option 3, if you tried to use let func: ((Thing) -> Bool) -> Int = myArray.count, that would be a warning or an error (because it's an ambiguous reference), which is different than the current world.

Jordan brings up a few problems with it though:

2 Likes

I thought about this problem some more and came up with the following rule that I think would greatly improve readability of functional code in Swift:

When referencing a function, all of its external parameter labels must be listed.

With this rule in place, functions like + or max can be used ergonomically as they do not expose any external parameter labels. All the other functions that expose external parameter labels expose them for a good reason and so they should always be listed at the point of use.

11 Likes
Terms of Service

Privacy Policy

Cookie Policy