Hi everyone! This topic has come up several times over the years, but I've started digging into an implementation recently and wrote up a first draft of a proposal to flesh out a bunch of the details.
Specific areas where I'd like assumptions validated (or don't have the expertise to fill out all the information) have been called out in italics. I of course welcome broad feedback about the proposal, and if anyone can spot additional corners where compound name syntax will have to be explicitly supported, please don't hesitate to raise it below.
Introduction
This proposal introduces a syntax for defining funciton-typed variables which have compound names (i.e., names with argument labels). This allows the call sites of such variables to achieve the same clarity that is achieveable with func
declarations.
Previous discussions:
[Update + Commentary] SE-0111: Remove type system significance of function argument labels
Extending declaration names for closures
Include argument labels in identifiers
Motivation
SE-0111 removed the type-system significance of argument labels, instead relegating them to part of the name of closure and function variables. Shortly after acceptance, it became clear that this resulted in a usability regression. Closure parameters and variables could no longer supply argument labels to their parameters, resulting in a reduction of call-site clarity:
// Pre-Swift-3:
let pow: (base: Int, exponent: Int) -> Int = { ... }
let eight = pow(base: 2, exponent: 3)
// Post-Swift-3:
let pow: (Int, Int) -> Int = { ... }
let eightOrNine = pow(2, 3)
The post-Swift-3 state of affairs has persisted to this day, leaving closures unable to benefit from argument labels, which are regularly listed amongst users' favorite features of Swift.
With some APIs introduced in the latest round of Apple's operating systems, this deficiency will become more apparent than ever. Types have begun adopting the "struct-with-closures" pattern in place of delegates/protocols, which, if the pattern continues to be used, will eventually degrade the readability of call sites throughout codebases.
Proposed solution
This proposal would adopt a modified version of the first step of the plan Chris Lattner proposed back when SE-0111 was originally accepted (edited for formatting):
The modification is to drop the commas from Chris's proposed syntax in order to utilize the existing syntax for referring to functions and methods with compound names.
Thus, under this proposal, Chris's example would become:
var op(lhs:rhs:): (Int, Int) -> Int
x = op(lhs: 1, rhs: 2)
func foo(opToUse op(lhs:rhs:): (Int, Int) -> Int) {
x = op(lhs: 1, rhs: 2)
}
foo(opToUse: +)
The resulting call site is far clearer than if we had just the base name.
Detailed design
Grammar
The language grammar will be extended in several locations to accept compound names where today only identifiers are accepted. These changes to the grammar will make use of the existing argument-names
production:
argument-names → argument-name argument-names_opt
argument-name → identifier ':'
Specifically, the following productions will be replaced with equivalents that accept compound names
// Old
variable-name → identifier
// New
variable-name → identifier '(' argument-names ')'
// Old
external-parameter-name → identifier
local-parameter-name → identifier
// New
external-parameter-name → identifier '(' argument-names ')'
local-parameter-name → identifier '(' argument-names ')'
// Old
tuple-element → identifier ':' expression
// New
tuple-element → identifier '(' argument-names ')' ':' expression
// Old
tuple-pattern-element → identifier ':' pattern
// New
tuple-pattern-element → identifier '(' argument-names ')' ':' pattern
// Old (tuple types)
element-name → identifier
// New
element-name → identifier '(' argument-names ')'
Additionally, we will introduce the following productions into the grammar :
pattern → compound-name-pattern type-annotation_opt
compound-name-pattern → identifier '(' argument-names ')'
primary-expression → identifier '(' argument-names ')'
Note that the last production appears to be accepted by the compiler already, although it is not listed in the language reference.
I couldn't find anywhere in the official language documentation that allows the existing syntax for compound names in primary-expression position; is this an oversight on my part, or should it just be added now, separate from this proposal?
Typing rules
The typing rules are relatively straightforward:
- Any value with a compound name must have function type (or optional function type).
- The number of labels must match the number of arguments to the function type.
If these rules are violated, the compiler will produce a diagnostic.
If the user defines a variable with a compound name that does not have function type, a fix-it will be offered to truncate the name to just the base name. If not enough argument labels are provided, a fix-it will be offered to insert additional '_:
' labels. If too many argument labels are provided, a fix-it will be offered to remove the extra labels.
Offering fix-its here might be erroneous. We can't necessarily know where the excess/missing argument labels are meant to go—is it better to just not offer the fix-its at all?
Valid positions for compound names
Compound names can be used in any position where a function-typed variable is being given a name by which it can later be referenced. This includes the following
- Variable declarations:
struct S {
let f(x:): (Int) -> Void = { print($0) }
func foo() {
let g(y:): (Int) -> Void = { print($0) }
g(y: 0) // 0
self.f(x: 0) // 0
}
}
- Pattern-match bindings
enum E {
case c(f: (Int) -> Void)
}
func foo() {
let e: E = .c({ print($0) })
switch e {
case .c(let f(x:)):
f(x: 0) // 0
}
}
- Tuple types and expressions
let t: (f(x:): (Int) -> Void, x: Int) = (f(x:): { print($0) }, x: 0)
t.f(x: t.x) // 0
- Function, initializer, and subscript parameters
func foo(callback callback(data:): (Int) -> Void) {
callback(data: 10)
}
Are there any other spots in the language that I'm overlooking here?
Compound names are not permitted to appear in any position which would result in them being the external label to a function parameter. This means that following declaration is malformed:
func fetchImage(from url: URL, callback(image:): (UIImage) -> Void) { ... }
There are several reasons for this rule.
-
For one, it allows us to avoid the potential for unboundedly "deep" declaration names such as
f(g(h(x:):):)
. -
Additionally, though, it is the author's determination that allowing constructions such as this do nothing to advance the goal of call-site clarity for the labeled function value. The name provided in an argument label is inherently divorced from the name used to call the function.
There may be minor benefit at the point where the function value is passed with a label, but it may also potentially result in users defining functions with awkward argument labels, e.g, if they don't realize that in the above example 'callback(image:)
' is both the internal and external name. There is also no clear path forward in this case to extending the use of compound names inline (see Future directions).
This proposal does not close off the possibility of such labels being allowed in the future, and the author suggests that such a feature be evaluated separately from the issue at hand.
When arguments such as the above are defined, the compiler will produce a diagnostic and offer a fix-it of the form:
Error: compound names cannot be used as external argument labels.
Fix-it: Insert 'callback '
in order to provide non-compound argument label.
In practice, this means that enumeration cases are not allowed to use compound names for associated value labels, since these are always API.
Calling variables with compound names
A value defined with name f(x1:...xn:)
that has non-optional function type may be called exactly the same as a function declared as
func f(x1: T1, ..., xn: Tn) { ... }
The call site for f
in both cases appears as:
f(x1: arg1, ..., xn: argn)
If f(x1:...xn:)
has optional function type, the call site instead appears as:
f?(x1: arg1, ..., xn: argn)
Referencing compound names
Like functions and methods, variables with compound names may be referred to by either their base name, or their full name. Thus, the following is valid:
struct S {
var foo(x:): (Int) -> Void = {}
}
let s = S()
let foo1 = s.foo
let foo2 = s.foo(x:)
Since argument labels are part of the name of the variable, it is perfectly valid to define multiple variables in the same scope/type which share a base name:
struct Foo {
let f: () -> Void
let f(x:): (Int) -> Void
}
These differences should be naturally resolved when calling the function. If needed when referencing the variable, users can differentiate by spelling out the full name, such as someFoo.f(x:)
or by providing a type annotation, e.g., let g = f as () -> Void
. The
This proposal does not adopt any rules which allow true overloading of variables with compound names, i.e., two variables with the same base name and argument labels, but different types. Such a rule could be considered in a later proposal (see Future directions below).
Synthesized initializers
Since compound names cannot be the external argument label for an initializer argument, a bit of care is required when synthesizing the initializer. This proposal would adopt a rule which simply drops the argument labels from the name of any properties which have compound names to create the argument label. In practice, this would work as follows:
// User writes:
struct S {
let f(x:) = (Int) -> Void
let g(y:) = (Int) -> Void
}
// Compiler synthesizes
extension S {
init(f f(x:): (Int) -> Void, g g(y:): (Int) -> Void) {
self.f(x:) = f(x:)
self.g(y:) = g(y:)
}
}
// Resulting initializer call
S(f: { print($0) }, g: { print($0 + 1) })
This can potentially lead to some ambiguity if two closure variables with compound names have the same base name. However, the proposal author makes the judgement that this will be unlikely in practice, and so does not justify supporting external argument labels with compound names.
Instead, the compiler can offer a warning if two compound names would have the same label in the synthesized initializer, with the option for the user to define an explicit initializer to silence the warning.
KeyPath
and compound names
Currently, key paths cannot refer to methods, so there is no compound name syntax in key paths. This proposal introduces the natural syntax for referring to members with compound names:
struct S {
let f(x:): (Int) -> Void
}
let path: KeyPath<S, (Int) -> Void> = \.f(x:)
Source compatibility
This is a purely additive proposal and maintains full source compatibility.
Effect on ABI stability
TBC: I haven't reached the point in my implementation where I have had to make any changes to ABI-dependent structures, so I can't yet comment on the impact here. Anyone who is more knowledgable on this front, I encourage you to call out any major issues you anticipate!
Effect on API resilience
Changing the name of a public
symbol is an API-breaking change, and would remain so under this proposal. Specifically, adding or removing argument labels—or changing an existing argument label—is not API-safe in the general case.
Future directions
Allow declaration of argument labels inline with the function type
With this feature fully implemented, we can move to the second step raised in the post-acceptance discussion of SE-0111. This would allow a declaration such as:
var pow: (base: Int, exponent: Int) -> Int = { ... }
to be a syntactic sugar for
var pow(base:exponent:): (Int, Int) -> Int = { ... }
This should be a relatively straightforward extension of the groundwork laid in this proposal.
Allow external argument labels to have compound names as well
As discussed in Valid positions for compound names, external argument labels cannot have compound names, so the following declaration and call are invalid:
func fetchImage(from url: URL, callback(image:): (UIImage) -> Void) { ... }
// ...
fetchImage(from: someURL, callback(image:): {
imageView.image = $0
})
Enable overloading for variables with compound names
This proposal supports variables/properties which share a base name, such as:
struct Foo {
let f: (Int) -> Void
let f(x:): (Int) -> Void
}
In theory, this could be extended to allow overloading of these properties, so that one could write:
struct Foo {
let f(x:): (String) -> Void
let f(x:): (Int) -> Void
}
Allowing the type context at the call/reference site to disambiguate.
Extend optional
requirement support for protocol requirements
Currenly, the optional
modifier for protocol requirements may only be applied to member of @objc
protocols. However, since compound function labels can satisfy protocol requirements, and can also be applied to optional function types, one could imagine a pure Swift version of optional
, where the protocol:
protocol P {
optional func foo(x: Int)
}
is roughly equivalent to something like:
protocol P {
var foo(x:): ((Int) -> Void)? { get }
}
extension P {
var foo(x:): ((Int) -> Void)? { nil }
}
@DevAndArtist has raised this possibility before, and with the addition of compound variable names it would become a generally applicable feature for any function requirement.