[swift-evolution] When to use argument labels (a new approach)
Dave Abrahams
dabrahams at apple.com
Thu Feb 4 00:17:18 CST 2016
Okay, now I'm finally giving this its due—sorry, too much pressure
earlier in the day...
on Wed Feb 03 2016, plx <swift-evolution at swift.org> wrote:
> After reading these guidelines and seeing the responses I am glad to
> see some consideration given to argument labeling.
>
> After thinking it over, I think the rules *I* would like to see can be
> expressed very straightforwardly much more can be made much A minor
> point, but I think it’s important to distinguish between
> single-argument functions and multi-argument functions; doing so seems
> to simplify the rules (even if there are more of them).
>
> Everything that follows is my preferences, but I generally agree with
> Erica’s suggestions in the concrete cases.
I'll have to go back and have a look at that
*goes back to have a look*
Are there cases that add something significant to what I've already
written? I didn't see any. If you're saying what I wrote can be
improved by covering specific examples, please tell me which ones.
> I also think the emphasis on quasi-grammatical rules-and-roles is
> something of a dead-end for design guidelines and won’t include such
> considerations in what follows.
Characterizing what we've done in the proposed guidelines as emphasizing
“quasi-grammatical rules-and-roles” seems like an unnecessary and
slightly muddled potshot. The grammatical stuff in the current document
is not in any sense “quasi” and has no connection to the admonition to
clarify roles, which I consider to be important. That said, I'm not at
all attached to presenting the guidelines in terms of grammar, so I'm
happy to see how your approach works.
> ## RULES
>
> ### I. Single-Argument Functions:
>
> #### RULES:
>
> - general rule: don’t label the first argument
> - exceptions:
> - (a) the argument has a default value (`removeAll(keepCapacity: Bool = default)`)
> - (b) the function acts-like a constructor (covered in your rule 2)
> - (c) the “ecosystem rule” (see section III)
> - (d) the semantics of the argument are non-obvious (see below)
>
> #### REMARKS:
>
> I’m not sure (d) actually exists, though; every concrete example I can
> think up either falls under rule (b) or rule (c). It may not actually
> need to be a rule (other than as, perhaps, the underlying motivation
> for rules (b) and (c)).
> My intent with (d) was to address a similar concern as in Erica’s
> `init(truncating …)` and `init(saturating …)`: “if a reasonable reader
> would be unclear which of N plausible implementation choices
I think/hope you mean “semantic” rather than “implementation” here.
> you are making, you may wish to label the argument, even if you only
> have a single such function”…but, again, it’s hard to find any
> examples for (d) that aren’t also some mixture of (b) and/or (c).
Okay, well the fewer (ahem) guidelines, the better. Let's pretend you
didn't propose (d) and see how well it works.
> ### II. Multi-Argument Functions:
>
> #### RULES:
>
> - general rule: label all arguments
> - exceptions:
> - (a) omit the first label whenever the first argument is the
> semantic focus, and the other arguments are some mix of “details,
> adjustments, or modifiers”
This seems to be a different way of expressing something I was getting
at with the guidelines I posted to start this thread. I worry that what
it means for an argument to be “the semantic focus” is too vague. Why
is it an improvement over what I wrote?
> - (b) omit labels entirely whenever argument-ordering is irrelevant
> to the output (see below)
I don't think you mean this case:
func f(answer answer: Int = 42, message: String = "Hello, world") {
print("\(message)! The answer is \(answer)")
}
Why is this an improvement over the way it's phrased in the original
guidelines proposal: “when the arguments are peers that can't be
usefully distinguished”?
> #### REMARKS:
>
> For (a), the assumption is that we have a general consensus that “in
> methods for which one of the arguments is the semantic focus, that
> argument should be the first argument”; this seems pretty widely
> followed.
I think using known and well-defined terms like “sentence” (or even
lesser-known but well-defined terms like “clause”) is probably much
better than using an ill-defined concept like “argument is the semantic
focus.” Even if you can define this concept clearly, it would have to
offer some very compelling advantages to be an improvement over
something that is already well-established. What are those?
> This rule seems to cover e.g. `addObserver(_:forKeyPath:)` and
> `addObserver(_:selector:name:object:)` and `encodeObject(_:forKey:)`
> and `present(_:animated:completion:)` (née
> `presentViewController(_:animated:completion:)`), and so on.
>
> A point to bring up is that under these rules, the “evolution” of a
> name would be different: the just-so story for how
> `addObserver(_:forKeyPath:)` came to be so-called is that it *began*
> as `add(observer:forKeyPath:)`, but b/c the `observer` argument is the
> semantic focus it "made sense to move `observer` into the method
> name”; that is, the assumption is that functions like
> `addObserver(_:forKeyPath:)` are considered to be exceptions to the
> "base convention” and need to be justified.
Okay, I understand it, but I'm not sure why it's better. Please explain
why this is an improvement over the other approach.
> Also note that "counter-examples" to rule (a) are anything for which
> no one argument is easily-identifiable as the semantic focus.
>
> EG, in a function like:
> `adjudicate(plaintiff:defendant:circumstances:)` we can colorably
> claim `circumstances` is a modifier-type parameter, but we don’t—and
> shouldn’t!—treat either `plaintiff` or `defendant` as the
> semantic-focus. If you have two focuses then you have no focus, as it
> were.
>
> For (b), the intuition is that whenever argument-order is irrelevant,
> arguments should be unlabelled; thus e.g.:
>
> - min/max: don’t label the arguments
> - hypot: don’t label the arguments
> - copysign: ideally, label the arguments
> - atan2: ideally, label the arguments
Those last two may draw some quibbles from the math domain experts, but
I agree with the spirit.
>
> …and so on. Note that these examples are all "free functions”; there
> don’t seem to be many natural examples that *aren’t* free
> functions.
colorMixer.blend(color1, color2)
track.fade(from: initialVolume, to: targetVolume)
> Also, please don’t be mislead by your familiarity with
> e.g. `copysign` and/or `atan2`; they are used here to illustrate a
> general principle (argument-ordering) only, but in practice such
> highly-familiar “legacy functions” might be best-off given
> special-case handling.
Right, so we'd want different examples.
> ### III. Naming Functions/Ecosystem Rule
>
> The previous sections essentially assumed the function names are
> already-chosen (in line with existing conventions) and voice specific
> argument-labeling preferences.
>
> This section deals with a few changes to how function names should be chosen.
>
> The over-arching consideration is what I’ve been calling the
> “Ecosystem rule”: whenever a method a member of a “method family"—or
> could foreseeably become a member of such—one should aim for
> consistency in the base name, and use argument-labels as necessary;
> note that method families need not *require* argument labels:
>
> `contains(_: Point)`
> `contains(_: Line)`
> `contains(_: Shape)`
>
> …but they *may* require them, as for example in the `login` function
> that has already been discussed.
>
> The “ecosystem-rule" can also be applied somewhat more-broadly;
> consider the following name suggestions:
>
> `animate(duration:animations:)`
> `animate(duration:animations:completion:)`
> `animate(duration:delay:options:animations:completion:)`
> `animateUsingKeyFrames(duration:delay:options:animations:completion:)`
> `animateUsingSpring(duration:delay:damping:initialVelocity:options:animations:completion:)`
>
> …where the first three form an obvious family, and the next two are
> obvious “cousins” of that family due to choice of base names.
>
> A corollary of this policy is that the rule (3) suggestion—of omitting
> something like `…ForIdentifier...` or `(forIdentifier:…)`—will
> sometimes be overruled out of ecosystem concerns, but I suspect this
> will be somewhat rare in practice.
>
> For example, consider the following naming suggestions for the “tracks” example:
>
> // solo method (not part of any family)
> asset.trackWith(trackID)
>
> // family
> asset.allTracksWith(mediaCharacteristic: …)
> asset.allTracksWith(mediaType: ...
>
> // the below, instead of `trackWith` or `track(
> asset.firstTrackWith(mediaCharacteristic: ...)
> asset.firstTrackWith(mediaType: …)
>
> …or the same again, but perhaps dropping the `With` if that’s the overall preference.
>
> In any case, the overall goal behind the "ecosystem rule” is that
> similar things should be named similarly, and when semantic
> differences are small-enough it makes sense to use argument labels to
> make distinctions; different base names should be for functions that
> are at least a little different from each other.
This “rule” seems pretty darned vague, even after all this explanation.
I don't see how it could possibly be stated succinctly,
Furthermore, as I wrote to Erica, I have concerns about anything that
gives special treatment to method families, specifically:
* I'm wary of adding anything that encourages the creation of
“method families,” which have a higher cognitive overhead than many of
the alternatives.
* A guideline that forces you to change an existing non-overloaded API,
just because you are adding an overload, is problematic.
> ## GENERAL REMARKS
>
> Note that with the way I’ve tried to formulate these rules the Swift
> standard library should largely stay as-is. In particular:
>
> - methods without an identifiable “semantic focus” seem rare in a
> standard-library context; IMHO they occur naturally, but only really
> within UI/application-level code, not “basic building blocks” code
> - "method families” seem somewhat unnatural in “Swift-y” code outside
> of a small number of special-case scenarios (`contains`, various
> `init` families, etc.); they seem more common in UI/application-level
> code (e.g. for Objective-C interoperation), as default arguments cover
> most of the motivating use-cases
>
> …and most of the intent in these rules is to free up some room in the
> guidelines so that application-level code can be written to the
> guidelines without going through bizarre contortions (e.g. no one
> would ever have *chosen* `func
> dismissViewControllerAnimated(_:completion:)`, and we shouldn’t have
> to chose between either (a) using equally-awkward constructs in our
> own code or (b) being “non-guideline-compliant”).
I think we can achieve that goal with succinct, well-defined
guidelines... provided we choose them correctly. That's why I started
the thread.
> ## REMARKS ON RULE 3
>
> Separately, I think rule 3 is a hair too coarse to be a good guideline as-stated.
>
> I would split the “asking for X by name/identifier/etc.” into two cases:
>
> - (a) asking for X by some well-known/canonical $ID (e.g., such that
> it is a *major* error if no X is found for $ID)
> - (b) asking for X by some identifier (without a strong expectation as
> to whether or not such an X will or won’t be found)
>
> …and at least as a code-reader:
>
> - I have no objection to the proposed rule (3) in scenario (a)
> - I find rule (3) very odd in scenario (b)
> - I think very differently about scenario (a) and scenario (b), and
> would thus prefer that they look different
I see your point, here. For me, the most obvious way to distinguish
these would be by using a verb like “find,” “search,” or “seek” for
scenario (b). I'm not sure the difference needs to be expressed by
a different argument labeling choice—but I see how it could be. Will
have to give this one some more thought, thanks.
>> On Feb 2, 2016, at 6:32 PM, Dave Abrahams via swift-evolution <swift-evolution at swift.org> wrote:
>>
>>
>> This thread is related to the review of new API guidelines, but it's not
>> a review thread; it's exploratory. The goal is to come up with
>> guidelines that:
>>
>> * describe when and where to use argument labels
>> * require labels in many of the cases people have asked for them
>> * are understandable by humans
>> * preserve important semantics communicated by existing APIs.
>>
>> Here's what I'm thinking
>>
>> 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.
>>
>> 2. Words that describe attributes of an *already-existing* instance
>> should go in the base name rather than in a label:
>>
>> a.tracksHavingMediaType("Wax Cylinder") // yes
>> a.removeFirstTrackHavingMediaType("BetaMax") // yes
>>
>> a.tracks(mediaType: "Wax Cylinder") // no
>> a.removeFirstTrack(havingMediaType: "BetaMax") // no
>>
>> [yes, we could use "With" instead of "Having", but it's more
>> ambiguous]
>>
>> Words that describe attributes of an instance *to be created* should
>> go in argument labels, rather than the base name (for parity with
>> initializers):
>>
>> AudioTrack(mediaType: "BetaMax") // initializer
>> trackFactory.newTrack(mediaType: "Wax Cylinder") // yes
>>
>> trackFactory.newTrackWithMediaType("Wax Cylinder") // no
>>
>> 3. (this one is separable) When the first argument is the *name* or
>> *identifier* of the subject in the base name, do not label it or
>> describe it in the base name.
>>
>> a.transitionToScene(.GreatHall) // yes
>> a.transitionToSceneWithIdentifier(.GreatHall) // no
>>
>> let p = someFont.glyph("propellor") // yes
>> let p = someFont.glyphWithName("propellor") // no
>> let p = someFont.glyph(name: "propellor") // no
>>
>> Thoughts?
>>
>> --
>> -Dave
>>
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
--
-Dave
More information about the swift-evolution
mailing list