This looks like a huge improvement for consistency, call-site clarity, and swiftiness.
Strongly in favor!
NB. I’d also be in favor of actually requiring the first label if the function defines one. We already have a mechanism for omitting labels: func frob(_ f: () -> Void) can be written as frob { … }.
Strongly +1 to this, but I may be in the minority that would welcome mandatory labels where defined by the API authors.
Swift already supports label elision at the call site as declared by the API author using _. I believe that the sooner we move to enforcing consistency with this general syntax rule, the sooner API authors will move towards designs which clarify the call-site.
Conversely, the more leeway given via optional “suggested” practices, the longer we’ll be living with inconsistent APIs across first- and third-party code.
As a consumer of APIs, I really don’t want different ways of calling said APIs. I want a single canonical spelling that is clear and consistent at all points.
Thank you @xwu for writing this up! I'm probably in the minority in being ok with SE-0279 going through as it was, but I do think first parameter names ought to be an option at some point.
I'm against always requiring the label as-is mainly because I don't like the functionName parameterName syntax. It looks confusing, non Swift-like, and AFAIK "name otherName" isn't a syntax that exists anywhere in the language. Trailing closures are already confusing for new users, and I would rather not add to that noise by adding new syntax they have to visually parse.
I'm unsure whether this has been considered, but what about something similar to function-reference syntax? functionName(parameterLabel:) { closure } Assuming it wouldn't be too complex to parse (big assumption
) , it seems a bit more like code and would still be source compatible.
But IMO the biggest advantage is that syntax already exists in the language for a directly related subject.
Comparison:
// My preferred formatting for short closures with SE-0279, but at the cost of losing the clarifying label.
thing.bind { otherThing }
set: { otherThing = $0 }
// Doesn't look right to me for Swift/Code
thing.bind get: { otherThing }
set: { otherThing = $0 }
// Looks semi-right, but takes an extra line which isn't always preferable
thing.bind
get: { otherThing }
set: { otherThing = $0 }
// IMO This gets the best of both worlds
thing.bind(get:) { otherThing }
set: { otherThing = $0 }
// Additional formats:
thing.bind(get:) {
otherThing
} set: {
otherThing = $0
}
thing.bind(get:)
{ otherThing }
set: { otherThing = $0 }
In terms of the migration story, if/when it was made a requirement or preference to use the first parameter name this seems like a gentler transition from the existing trailing closure syntax. It would be an easy fixit with familiar and consistent syntax compared to the floating label which is new and should be put different places depending on the situation.
I would expect the asymmetry of requiring parentheses around the first closure label but none of the others (in fact, they are forbidden to have them) would be far more likely to cause confusion than the mere juxtaposition of a function's basename and its first trailing closure label.
Among all of the examples that you pasted, I find this to be the most readable (by far) due to the symmetry of its labeling and its indentation, and all of the examples requiring parentheses and the various wrapping/indentation used to try to adjust for that made them harder to parse mentally:
thing.bind
get: { otherThing }
set: { otherThing = $0 }
So, I don't buy the argument that this particular juxtaposition isn't "Swift-like". We've already accepted SE-0279, which juxtaposes multiple trailing closures separated by only their label, but even without that, this is a natural and clean extension of that which removes special cases from the language. That, to me, is a more Swift-like characteristic (and the goal to strive for), not strictly limiting ourselves to syntactic neighboring pairs that already existed.
I agree for some multi-closure cases, but definitely not for single closures:
myArray.first
where: { $0 < 5 }
// or
myArray.first where: { $0 < 5 }
// v.s.
myArray.first(where:) {
$0 < 5
}
// or
myArray.first(where:) { $0 < 5 }
That syntax would naturally be supported if/we generalize support for compound names. However, when there are other arguments to pass, the result will look like this, which I don't think you'll particularly find very elegant:
func f(bar: Int, baz: Int, boo: () -> Void) { ... }
f(bar:baz:boo:)(42, 42) { ... }
Likewise, the example of bind(get:set:) would look like this (and, yes, the logic of compound names would be such that you would need to use an empty label):
bind(get:set:)
{ /* getter */ }
_: { /* setter */ }
...and if we have a function both with non-closure arguments and multiple closure arguments, then you get a very unfortunate call site indeed:
func g(bar: Int, baz: () -> Void, boo: () -> Void) { ... }
g(bar:baz:boo:)(42) { ... } _: { ... } /* yikes */
It should also be noted that, so long as the first trailing closure is unlabeled, we likely cannot make the syntax work inside a statement condition (such as if), which is one of the principal motivating use cases here.
I dont like the function reference with out the parens. To me the parens r important since they indicate that I am calling the func.
thing.bind()
get: { otherThing }
set: { otherThing = $0 }
thing.bind() get: {
otherThing
}
thing.bind get: { // error; add ()
otherThing
}
That's really an orthogonal issue: Swift has supported [1, 2, 3].first { ... } instead of [1, 2, 3].first() { ... } since the very beginning.
Changing that rule would be out of the scope of this pitch, but if you don't like this style it's certainly something that a linter can enforce (I imagine there are tools out there already that can do this).
I would expect that to be calling the function reference, rather than the function directly, but that is admittedly a potential confusing / problematic issue.
Anyway what I was thinking of is something that would only involve the first closure label
func g(bar: 42, baz:) {} boo {}
Though similar to @masters3d I would be comfortable with the label being around after parens:
func g(bar: 42) baz: {} boo: {}
I guess my primary concern is to me this looks too much like plain text:
thing.makeCall completion: { $0 < 4 }
Although that's normal in some languages, (Python) it's abnormal in Swift. I believe Swift is consistently formatted as
sigil(s) name||value, sigil(s)
A floating label breaks that pattern and IMO should only be added very intentionally in basic syntax (excluding something like DSL support)
Enforcing parens at call sites, if that makes you more comfortable, is a straightforward thing for a linter to do, as I mentioned. It's not an unreasonable thing to want, and it applies to the existing trailing closure syntax also. Go for it :)
Ultimately, I think you're among the group who simply find the proposed spelling "unsettling" (as described in the proposal text), but I think once the novelty wears off it'll seem quite natural, since it's a generalization of an existing rule.
This is also why, instead of charging full on to making the compiler warn about using the syntax we already have, I'm proposing that we give the community time to coalesce around a set of best practices and styles; actual hands-on experience over time will guide us here. As you say, there are some situations in which the proposed spelling here is a clear win, and others where some people are on the fence; as with other aspects of closure syntax, not one style will be best for every use site: experience shows that we can trust users to pick what works best for them when we give them the option (which currently we do not in the case of first trailing closures).
(Incidentally, as we generalize other features, you are bound to see more instances where words are juxtaposed with each other. Off the top of my head, when we generalized opaque types, you'll start seeing spellings such as 42 as some FloatingPoint. These things will just fall naturally out of the existing rules. It's also rather amusing that we are now worried about call sites reading too well, when the API naming guidelines specifically recommend call sites that read fluently: "Prefer [to] make use sites form grammatical English phrases.")
I agree that generalizing support for compound names will give another way in which we can call first(where:), and that it's perfectly reasonable in that case; it doesn't work well for more complicated callers as I showed you. Therefore, I don't think it addresses the same problems as would generalizing support for labels to the first trailing closure. (I do support generalizing support for compound names though!)
If you're arguing that instead of generalizing any rules, we should invent a third kind of syntax specifically for the first trailing closure, I simply don't see how that could be justified.
Sure, but it generalizes a rule in a way that contradicts other existing rules and expectations. That's why people (at least me) are uneasy with it, not simply because it's unfamiliar. "you'll get used to it" is just as valid an argument to not make any change at all ![]()
In this context that's comparable to
public static var thing: String
To clarify, instead of sigil per-se I really intend to mean any syntactical mark or keyword that's not a name/value. struct, public, =, var, :, as, some where, (, {, etc.
Only if you require all the label names, not just the first. Yes that would be a different rule than a function reference, but it's a new rule for a new use case of an existing syntactic pattern rather than adding a new syntactic pattern.
Anyway I'm not happy with
thing.update(with: 4, completion:) { //the completion }
But personally I would prefer it over
thing.makeCall completion: { //the completion }
Maybe there's another option? ![]()
No, it isn't, for the reasons outlined in the "Motivation" section. I am proposing this feature because the status quo causes problems, not because I'm not "used to" trailing closure syntax.
Labels aren't standalone names; they're part of a name, and with this proposal they wouldn't be allowed willy-nilly next to anything other than the rest of the name exactly as declared.
It's fine if you don't like this particular use site. This proposal wouldn't require it, and again, if you prefer thing.makeCall() completion: { ... } I agree that that's perfectly reasonable, and this proposal 100% would enable this for you.
I think it's also reasonable to want the default first-party tooling to prefer interposing some sort of punctuation or newline between the base name and a label; the core team can certainly adopt that stance if they agree.
I'm not sure what rules are contradicted, but I think we're ultimately saying the same thing.
The proposal makes possible use sites such as [1, 2, 3].first where: { ... }. This has a novel appearance because the label is next to the base name without some punctuation between them. You have an expectation that there would be punctuation there, and the lack of that punctuation makes you uneasy because it goes against an expectation you have.
However, there's no general rule in Swift that has to be violated to make this novel appearance possible. The expectation you have arises strictly because such an appearance happens not to have arisen before. Therefore, I reason, once this appearance is no longer novel, the expectation will go away, and you'll no longer then experience the unease that comes with an unmet expectation.
(I can say that, in my personal experience, I was uneasy at the appearance of such a use site for a bit, and now that I've spent some time writing up this proposal, it has gone from feeling just a little wrong to feeling very right.)
Many syntactical features in a language are one-off things. For example, angle brackets are for generics only. Just because nothing else in the language uses angle brackets, it doesn't mean generics shouldn't have used it.
See 1 < 2 ![]()
not 1<2> tho
I guess I didn't express myself clearly.
Subscripts perhaps would be a better example: Nothing but subscripts use square brackets, but it doesn't mean using square brackets for subscripts is bad.
EDIT:
Just to further clarify myself, and proof against any misunderstanding, by this subscript square bracket thing, I mean something like variableName[index], so array literals and dictionary literals don't count.
You're just here to stir things up, aren't you?
Thanks for writing this up @xwu! I think you motivate things nicely.
Oh, I beg you, please don't imagine drop(where:)! The drop operations are part of a family slicing operations, including the prefix and suffix methods, which makes what I'm sure you imagine drop(where:) to mean either unimplementable or misleading.
As I've said before, the problem with drop(while:) isn't the fact you don't see the argument label, it's that it doesn't tell you which end of the sequence is being dropped. Maybe also that people don't remember that “drop” is part of this slicing family. The words while or where are superfluous in this context, and nearly equivalent. “While” is only slightly better because it implies that it takes the longest prefix where the predicate is satisfied. IMO the chance of anyone interpreting
a.prefix(where: { $0 < 10 })
as anything other than the longest prefix of a with all elements less than 10 is vanishingly small. What other deterministic result would be reasonable?
If I was designing this over again, I probably would have spelt these all with labeled subscripts (names off the cuff):
x[prefix: 3] // x.prefix(3)
x[suffix: n] // x.suffix(n)
x[sansPrefix: 5] // x.dropFirst(5)
x[sansSuffix: 6] // x.dropLast(6)
x[prefix: { e in e < 0 }] // x.prefix { e in e < 0 }
x[sansPrefix: { e in e < 0 }] // x.drop { e in e < 0 }
This arrangement makes it clear that the operations are slicing, and furthermore makes them usable for in-place mutation on MutableCollections:
x[prefix: 100].sort() // sort the first 100 elements of x.
And I make no apologies for the fact that you can't use trailing closure syntax on these without ambiguity.
Functionality, clarity, and consistency beat cute syntax every time.
Or I didn't
.
The point I was trying to make is this introduces a new type of syntax spelling for users to visually parse and that should be done intentionally and taken seriously. My point isn't "don't add any new syntax". Angle bracket spelling for generics enable one of the most powerful features in the language and have become a semi-standard way to spell the feature across numerous languages. A first-param label adds clarity in some circumstances that can also be accomplished/improved in other ways which, again, I think it's a good thing to try and do, but would prefer it be done in a manor that's more familiar in Swift.
Both generics and subscripts are great examples because they can often be a stumbling block for new users and are very useful features. They're also spelled similarly to other language features with different symbols so the amount of potential confusion is worth the tradeoff. My question is whether adding a new spelling is worth it for this feature, or if there's another way it can be spelled that would not be as unfamiliar.