Require parameter names when referencing to functions

It is non-uniform, but there's no way around that without changing (x:y:) to something else, like [x:y:]. I don't think a change like that currently is, or should be, on the table.

I think the idea of the keypath syntax for methods was that it would get some representation of an unbound instance method from a type, e.g. \String.removeFirst(_:). I suppose we could extend that to work on instances too (\myString.removeFirst(_:)), but that would be a change from key paths.

Operators are actually an interesting question. I think we could justify keeping the existing behavior for them; they live in a separate namespace from properties, there's an ambiguity between prefix and postfix operators that would need some additional design to resolve, and they're special in various other ways too.

5 Likes

I tend to agree. I just figure someone should at least ask the question. It’s something to mention in the proposal.

What about this one?

I wouldn’t want to choose an approach that leaves a dangling problem for no-arg functions and requires yet another across-the-board change. Measure twice, cut once.

1 Like

+1 for the parameter names being always required, including init(_:) and similar. These names play a significant semantic role in Swift and so it is very important for the programmer to always see them.

2 Likes

Maybe I've done too much metaprogramming in my life but I don't see that much difference between metatypes and types here.

Thinking through some of the consequences of this pitch, I find it detrimental to code for both readers and writers.

Simple calls like:

let c = zip(a, b).map(max)

would become:

let c = zip(a, b).map(max(_:_:))

where the extraneous symbols after max make it more mentally taxing to remember how to write, and more challenging to read.

This change would make the Swift language objectively worse, and I am strongly opposed to it.

21 Likes

Currently, \ reaches "down" a level—you use the type to form an instance key path, you (would) use the metatype to form a type key path, etc. \myString.removeFirst(_:) would muddy that rule.

1 Like

Just thinking out aloud, would there be a trade-off worth making to still allow shorthand identifiers like max (and, by extension, all operators) to refer to functions that take all their arguments without names?

That would incur some type checker overhead (having to consider the variants of form max(), max(_:), max(_:_:), etc), but much less than currently.

2 Likes

I can't agree more.

I absolutely understand the motivation here, but it comes at great cost to readability/writability. (Too great a cost, IMO.)

2 Likes

I agree with you on the readability point, but I'd imagine autocomplete would take care of writing out the argument separators in most cases.

3 Likes

Sure, but you write the code once and read it many more times thereafter — as such, more weight should be given to readability.

1 Like

Can it be done so that providing parameter names is optional? i.e. when compiler is unable to resolve the function it simply throws an error saying "unable to resolve, provide complete name with parameters"?

3 Likes

I would suggest, without any technical backing behind this idea, that we "just" eliminate type inference as way of resolving whether names refer to functions, and keep only syntactic resolution.

For example, if there is (1) a non-function declaration for foo and (2) a function declaration for foo (with any combination of parameters), then writing a reference to foo would unambiguously refer to #1. If there is no non-function declaration, but only one function declaration, foo would refer to that function. If there is more than one function declaration (with different parameters), the parameter keywords would be needed to disambiguate.

The point is that type inference doesn't come into that, so the combinatorial explosion problem shouldn't occur. (Right??)

One of the great things about Swift is that you don't have to write something if you know that Swift knows what you mean. Forcing parameter names to always be written out forces you to write them even when there's only one possible thing they could mean. I'd hate for that to happen. (And I agree with @Nevin's point about readability.)

6 Likes

Why would we need to require init(_:) or max(_:_:)? This would be pain in the butt and hurt readability, not even speaking about how many code points would be required to be migrated / rewritten.

I view the solution from a more flexible angle. Only require parameter names iff there is at least one label that would be critical for the resolution path or if you need explicitly provide an information about how much parameters your function overload has that you wish the compiler to pick from.

That way the above can stay as init and max unless you need the second part of the rule or would like to avoid the compiler from picking a labeled overload, only then you may want to use init(_:) or max(_:_:).

2 Likes

I oppose any implementation that forces reference to init(_:) or max(_:_:) but support an option to disambiguate when needed.

7 Likes

We could also allow the bare function name to refer to any functions which have no parameter labels; that is, foo could reference foo, foo(_:), foo(_:_:), etc. without allowing it to reference foo(bar:). That change would be slightly more breaking, but I think has more justifiable semantics. We don't currently allow typing as much of a property name as is required to disambiguate:

struct Baz {
    var foo: Int = 0
    var bar: Int = 1
}

print(Baz().f) // we only need the 'f' to know we're talking about foo!

I view this proposal as disallowing the same sort of construction for method/function names.

1 Like

I don't like this proposal.

The source of the problem comes from a lack of standardization of naming. As it is, the standard library often uses the name of return values as method names – that is, the name of what the method would be if it were a property. (first is an example.) There will be edge cases, but generally, disallowing that eliminates the confusion; my general solution is to prefix all such methods with get.

Any language with functions that can be referred to with constants/variables needs to tackle this. It's not obvious until you come across your own related problem – like Soroush has!

I want the method. My proposal to get it in is to start a precedent for better naming. As a property, it's count. As a method, it should be getCount.

I like this approach — disambiguate as needed.

Although expectation is named incorrectly (it is more accurately makeExpectation), I don't think it this is a problem in practice, because we make an expectation using the XCTestCase instance:

let expectation = self.expectation(description: "")

And while better naming would eliminate self for common usage, we'd still need to disambiguate using what Soroush is proposing, if we cared to store the method, and there were overloads.

extension XCTestCase {
  func makeExpectation(description: String) -> XCTestExpectation {
    expectation(description: description)
  }

  func makeExpectation(
    for predicate: NSPredicate,
    evaluatedWith object: Any?,
    handler: XCTNSPredicateExpectation.Handler? = nil
  ) -> XCTestExpectation {
    expectation(
      for: predicate,
      evaluatedWith: object,
      handler: handler
    )
  }
}

final class TestCase: XCTestCase {
  func test() {
    let expectation = makeExpectation(description: "")
    let makeExpectation = makeExpectation(description:)
  }
}

It's because of cases where overload disambiguation isn't needed, that I'm against this proposal.

let makeExpectation: (String) -> XCTestExpectation
makeExpectation = self.makeExpectation

I'm a big proponent of this (clarity at the point of use, even over brevity), but I think people have made really good points about it flipping from helpful to noise for functions with unlabeled arguments, like operators or max or even converting initializers. The danger in those cases is around overloading based on type, and that's always going to be a concern for Swift.

I'm against the "disambiguate as needed" approach because it makes it harder for library authors to change their code. If a method has a sensible base name, and you want to add functionality that would share the same base name but uses different argument labels, you shouldn't have to worry about whether someone might be referring to the method with just the base name. I think a lot of the reason this feels weird is because argument labels aren't considered part of a method's name in many other languages, but we do think they are in Swift and have been moving the compiler in that direction for a long time.

(I'm also against the "methods and properties should not have overlapping names" suggestion, but I think that's only one of the motivations here.)

EDIT: I forgot to post my conclusion: "so I support the middle ground where foo can refer to foo(), foo(_:), foo(_:_:), etc., but not to foo(bar:) or foo(_:baz:)".

DOUBLE EDIT: …even though, ugh, this is going to be annoying to implement.

31 Likes

What if we stop trying to do the 'fully disambiguated' case inline? Let's make a syntax for getting a fully qualified and unambiguous function/method that isn't necessarily meant to be used inside a call to map but can be used before and yields a value that you can pass to map