[swift-evolution] When to use argument labels (a new approach)

Dave Abrahams dabrahams at apple.com
Wed Feb 3 16:50:35 CST 2016


on Tue Feb 02 2016, Brent Royal-Gordon <swift-evolution at swift.org> wrote:

>> 1. If and only if the first argument could complete a sentence*
>>   beginning in the base name and describing the primary semantics of
>>   the call, it gets no argument label:
>> 
>>     a.contains(b)  // b completes the phrase "a contains b"
>
>>     a.mergeWith(b) // b completes the phrase "merge with b"
>> 
>>     a.dismiss(animated: b) // "a, dismiss b" is a sentence but 
>>                            // doesn't describe the semantics at all, 
>>                            // thus we add a label for b.
>> 
>>     a.moveTo(x: 300, y: 400) // "a, move to 300" is a sentence 
>>                              // but doesn't describe the primary 
>>                              // semantics, which are to move in both
>>                              // x and y.  Thus, x gets a label.
>> 
>>     a.readFrom(u, ofType: b) // "a, read from u" describes
>>                              // the primary semantics, so u gets no
>>                              // label. b is an
>>                              // option that tunes the primary
>>                              // semantics
>> 
>>   [Note that this covers all the direct object cases and, I believe,
>>   all the default argument cases too, so maybe that exception can be
>>   dropped.  We still need the exceptions for full-width type
>>   conversions and indistinguishable peers]
>> 
>>   Note: when there is a noun in the base name describing the role of the
>>   first argument, we skip it in considering this criterion:
>> 
>>      a.addObserver(b) // "a, add b" completes a sentence describing 
>>                       // the semantics.  "Observer" is omitted in 
>>                       // making this determination.
>> 
>> * We could say "clause" here but I think making it an *independent*
>>  clause doesn't rule out any important use-cases (see
>>  https://web.cn.edu/kwheeler/gram_clauses_n_phrases.html) and at that
>>  point, you might as well say "sentence," which is a more
>>  universally-understood term.
>
> I've been considering this for a bit, and I'd like to suggest a
> different way of thinking about things. This is kind of high-level and
> may need to be translated into concrete guidelines, but it's guided my
> thinking about good Swift method names recently.
>
> You can broadly separate parameters into operands and
> options. Operands slot into the "sentence" formed by the method name;
> options generally do not.
>
> Operands should generally *not* be labeled explicitly with the name of
> what's being passed in that slot; the name should fall out by
> grammatical implication. For instance, in:
>
> 	foo.remove(bar)
>
> The only sensible way of reading that is that `bar` will be removed
> from `foo`. If it were the other way around, it would have to be:
>
> 	foo.removeFrom(bar)
>
> A second operand should usually be labeled, but not explicitly—only by
> grammatical implication. Again, this is because the structure of the
> "sentence" should imply the role of each operand. So when we want to
> insert into an array, which has two operands, we say:
>
> 	foo.insert(bar, at: 0)
>
> The grammar used to label the first parameter, however, should be
> included in the method name itself. In a sense, the method's name
> should be thought of as "insert at", but the constraints of Swift
> syntax demands we use a parameter label for the "at".
>
> (There are, of course, the twin exceptions of omitting completely
> vacuous labels and labeling the meaning of operands whose type is not
> specific enough to imply their meaning. Both of these are at work in
> `reduce(_:combine:)`; it should theoretically be something like
> `reduce(_:with:)`, but `with` is vacuous and the type of the parameter
> is not strong enough to imply its meaning and it needs to be labeled
> `combine`.)
>
> Options, on the other hand, *are* explicitly labeled with the meaning
> of the parameter, because they aren't as directly connected to the
> sentence and it would be difficult to imply their role. Hence, in
> something like:
>
> 	foo.lexicographicalCompare(foo2, isOrderedBefore: <)
>
> The second parameter is an option and has to be explicitly labeled
> with its meaning. Options *often* have default values, but not always;
> for instance, the `animated` parameter in many UIKit methods is an
> option, but Swift does not give it a default value.
>
> Because options should be explicitly labeled with their meaning but
> operands should have their meaning implied, options usually have the
> same label and variable name, while operands usually have different
> labels and variable names. This is not *always* the case, but it's a
> pretty strong hint.
>
> *Usually* the first parameter is an operand and the other parameters
> are options, and so by default, Swift gives the first parameter an
> empty label and the remaining parameters a label matching their
> variable name. But you may have to override that in either direction
> if your method doesn't quite fit that norm.
>
> Incidentally, I would like to suggest one small change based on this
> conception: the `min` and `max` free functions should be called
> `minOf` and `maxOf`. This both reads slightly better and gives them
> distinct names from the `Collection` methods.

I believe everything you've written here is actually expressed, in
practice, by the guidelines I've suggested.  I went through many of the
same thought processes in working the guidelines out.  The trick is to
capture these thoughts in something simply stated that has the right
effect.  If you think the guideline I suggested doesn't have the right
effect, please demonstrate a case where your thought process would lead
to a different API choice.

> * * *
>
> As long as I'm here, a word about method names.
>
> I've seen some suggestions that method names should be only verbs, and
> so `-addSubview:` should be `add(subview:)`. I don't think this is
> useful or appropriate.
>
> What you need to understand about this is that the `Subview` in
> `-addSubview:` is not really labeling the parameter. It's connecting
> the method to the `subviews` collection. 

Exactly.  adding a subview is different, *in its primary semantics* from
adding, say, a gesture recognizer.

> Similarly, in `-addObserver:forKeyPath:options:context:`, the
> `Observer` is connecting the method to the (private,
> side-table-stored) observer list. This also reflects the fact that,
> even though these two operations both "add" something, they are
> massively different—`addObserver` and `addSubview` have almost nothing
> in common.
>
> Importing `-addSubview:` as `add(subview:)` would be like importing
> `-setNeedsDisplayInRect:` as `set(needsDisplayInRect:)`. The method
> name quite simply no longer describes the operation being performed.

Technically the required labels are part of the method name, but I think
we all recognize that the base name of the method (the part before the
opening parenthesis) is naturally more important to a person reading
swift code.  The idea of these guidelines is that the primary semantics
of the method should, whenever possible, be expressed in the base name
and the first parameter, and when the first parameter either not not
part of the primary semantics, or (with the base name) not enough to
express the primary semantics, it should be labeled.

-- 
-Dave



More information about the swift-evolution mailing list