[swift-evolution] Inconsistencies related to prepositions

Xiaodi Wu xiaodi.wu at gmail.com
Wed Aug 2 15:54:05 CDT 2017


On Wed, Aug 2, 2017 at 2:23 PM, Robert Bennett <rltbennett at icloud.com>
wrote:

> I guess I will accept that implicit types will serve to explain the
> purpose of prepositions and do not need to be written explicitly. But I’m
> curious if there are any other thoughts on the trend exemplified by
> `fetch(withRecordID:)` and the issue that the “how” is described but not
> the “what”,
>

`fetch(withRecordID:)` is a CloudKit API not subject to Swift Evolution.
Interestingly, the corresponding Obj-C function is called
`fetchRecordWithID:` and not `fetchWithRecordID:`, so clearly someone at
Apple chose the Swift name manually: a rote translation from Obj-C based on
Swift API guidelines would be `fetchRecord(with:)`, as the argument is of
type `CKRecordID`. This clearly deliberate renaming is not dictated by
Swift naming guidelines but a discretionary choice of the CloudKit authors
at Apple; if you feel this is a bug, then I guess you can file a Radar.

which a more serious problem in my eyes, and my proposal that methods like
> this be of the form `<subject>.<action><object>(<preposition><secondObjectIfNecessary>:)`,
> where the second object is necessary when type information alone does not
> convey its meaning, as is the case with a record’s ID.
>
> On Aug 2, 2017, at 1:49 PM, Xiaodi Wu <xiaodi.wu at gmail.com> wrote:
>
>
> On Wed, Aug 2, 2017 at 12:00 Robert Bennett via swift-evolution <
> swift-evolution at swift.org> wrote:
>
>> I agree with some of your points and disagree with others. You are right
>> that Swift did not get everything right when it “Swiftified” some
>> Objective-C names. However, I don’t think your criticisms are all correct.
>>
>> At some point, Swift decided to omit needless “helpful” names from method
>> names. This is a good thing. You don’t “string-capitalize” a string; you
>> capitalize it. And since types are known at compile time, there isn’t
>> really a need to name the type of every argument. This would hurt the
>> readability/Englishness of Swift. For instance, `append(contentsOf:)` — in
>> English, you would say “append the contents of that bucket to my array”,
>> not “append the contents of collection that bucket to my array”. Only
>> collections can have contents so you can omit that type, both in English
>> and in Swift. In short, when the argument label clearly implies the type of
>> the argument, there’s no need to explicitly state it. However when that
>> type is not clear then it should be specified.
>>
>> I think Swift did move in a weird direction with method base-names, as
>> shown in some of your examples. It relies too heavily on a shared verb
>> among methods with argument labels serving as the primary differentiators,
>> when sometimes part of the label should be attached to the verb. And for
>> arguments, it relies too heavily on type information to understand the
>> functionality; somewhat-hidden type information (inspectable in Xcode, but
>> not very obvious if you’re just reading quickly) should be made visible in
>> the argument labels where necessary.
>>
>> To your specific examples:
>>
>>
>>    1. `fetch(withRecordID)` leaves unanswered the question “what is
>>    being fetched?”, instead forcing you to infer the answer from the question
>>    “how is the thing being fetched, being fetched?” — “it is being fetched by
>>    record ID so I guess it must be a record”. Is there even such a thing as a
>>    record ID? I think not — there are just IDs, some of which are tied to
>>    records. I think that that method should be called `fetchRecord(withID:)`.
>>    This makes it clear that a) a record is being fetched and b) its ID is what
>>    is used to fetch it.
>>    2. `string.capitalized(with:)` it very unclear. As an uneducated
>>    programmer, I have no idea what you capitalize a string “with”. With a
>>    font? With some Unicode machinery? With gusto? (Ok that last one was a
>>    joke.) It’s not at all obvious that you capitalize a string with a locale,
>>    and hence I think that that method should be called `string.capitalized(withLocale:)`.
>>    I explain above why it should be string.capitalized and not
>>    string.stringCapitalized.
>>
>>
> This topic was reviewed extensively at the beginning of Swift Evolution.
> The idea is that type information does not need to be repeated in the
> label. Since the locale is of type Locale, “with” is preferred over
> “withLocale” because “withLocale: Locale” is redundant. As this has been
> long approved by the community, it’s not in scope to roll it back.
>
>
>>    1.
>>    2. `dict.remove(at:)` is a bit of a tougher case. It is mutating, so
>>    it has to be in the present tense. Yet it does return an element, so maybe
>>    it should state that? I think the deciding vote is that it’s
>>    @discardableResult, which means it should be treated as primarily removing
>>    the element; returning it is secondary. Maybe to indicate the returning of
>>    the element, it should be called `dict.pop(at:)`. “Pop" implies the element
>>    popped, and probably implies @discardableResult in most programmers’ minds
>>    as well.
>>
>>
> Brent had a draft proposal to revise the names of collection methods to
> improve the situation here. There is room for improvement.
>
>
>>    1.
>>    2. `date.timeInterval` is another tricky one. IMO the most natural
>>    choice is `date.timeIntervalSince(_:)`. I don’t think the date argument
>>    label is necessary because given a date, you can only compute a time
>>    interval since… another date. See the `array.append(contentsOf:)` example
>>    above. And although `since` is a preposition, it should be part of the
>>    method name, and not an argument label, because dates do not just “have”
>>    time intervals. A database can fetch a record, but a date does not have a
>>    time interval — it has a time interval since (another date).
>>    3. This should definitely be `forEach`. `array.each` would be another
>>    name for `array.everyElementSatisfiesCondition(_ f:
>>    (Element)->Bool))`. “For each” specifies that you will do something *for
>>    each* element as opposed to asking a question about each element. In
>>    English, “for each thing in my bucket…” will probably be followed by “do
>>    this thing”. You could say “for each thing in my bucket, it’s true that…”
>>    but you’d probably just say “everything in my bucket is…"
>>
>>
>> So, if I had to come up with my own rules for naming conventions:
>>
>>
>>    1. Methods should first state what they do, then how they do it. You
>>    fetch a record using an ID; you don’t fetch a thing using a record ID. Ask
>>    the question “does the calling variable have such-and-such a thing” or “can
>>    the calling variable do such-and-such a thing”, and be specific about
>>    exactly what the thing is. If yes, than that thing is a good candidate for
>>    a method name. A database can fetch, sure, but more specifically it can
>>    fetch records, and that’s about as specific as you can get. A date doesn’t
>>    have a collection of time intervals sitting around, but it can compute the
>>    *time interval since* another date. Strings can be capitalized (with a
>>    locale), dictionaries can remove (at an index), arrays can append (the
>>    contents of some collection).
>>    2. When the preposition/argument label makes it clear what sort of
>>    argument it takes, then it’s fine to omit the type. However if the argument
>>    label is vague, then the argument type should be included. In this case the
>>    type is not just a type specifier — it’s really part of the English
>>    description. You don’t say “append the contents of collection that bucket
>>    to my array”, but any sentence about capitalizing a string with a locale
>>    would use the word “locale” explicitly, either in the name of the variable
>>    or otherwise — “capitalize that string with your specified locale”,
>>    “capitalize that string with locale XYZ”, etc. The English language doesn’t
>>    really have the notion of implicitly locale-like objects the way it has
>>    implicitly collection-like objects such as buckets, bags, etc, so the fact
>>    that a locale is being used should be explicit while the fact that a
>>    collection is being used may be implicit.
>>    As an aside, I feel that “using Locale” would be better than “with
>>    Locale”, but that’s a conversation for another time.
>>    3. Three nouns may be required in a name, as in the fetch record
>>    example. The calling variable is always the subject, and there may be
>>    either one or two objects in play. If there are two, then one should go in
>>    the method name; the other may or may not be made explicit in the argument
>>    label, depending on clarity (see the above point). When a database fetches
>>    a record using its ID, you have only two choices: db.fetchRecord(with:) or
>>    db.fetchRecord(withID:). In this case, clarity would lead to the second
>>    choice, as you don’t know what you’re fetching the record *with*. In the
>>    date example, the calling date is the subject, and it is *producing a time
>>    interval since* another date. So again, two choices:
>>    date.timeIntervalSince(date:) or date.timeIntervalSince(_:). Since you can
>>    only find the time interval between two dates, `date:` can be omitted.
>>
>>
>> It’s hard to make hard-and-fast rules around this because language is far
>> from hard-and-fast itself. But I think these would be good guidelines.
>>
>>
>>
>> On Aug 2, 2017, at 11:44 AM, Jon Gilbert via swift-evolution <
>> swift-evolution at swift.org> wrote:
>>
>> When Swift 3 launched, it introduced a new concept of placing the
>> preposition inside the parentheses. (See discussion here:
>> https://lists.swift.org/pipermail/swift-evolution/
>> Week-of-Mon-20160208/009523.html).
>>
>> I'm fine with that, however this change got implemented in an
>> inconsistent manner, making Swift and its APIs more vague, thereby
>> decreasing code clarity.
>>
>> I was hoping to see these inconsistencies get cleared up with Swift 4,
>> however they were not. I realized that perhaps I could have spoken up and
>> taken more of an active role in Swift Evolution. So... better late than
>> never. Here are my thoughts, towards consistency and disambiguation in
>> Swift.
>>
>> Disclaimer: I’m sure the last thing anyone wants are more changes to the
>> APIs. However, I also don't think these inconsistencies should be left in
>> place forever. I have tried to go back and read through as much of the
>> relevant discussions on list as I could, and it seems like
>>
>> Please let me some give examples below. After that is some discussion and
>> a proposal.
>>
>> *EXAMPLES*
>>
>> Take for example the preposition "with." A preposition is meaningless
>> without a subject and an object. Look at the following three sentences:
>>
>>  (A) "A boy with the dog Molly walked across the street."
>>
>>  (B) "With the dog Molly walked across the street."
>>
>>  (C) "A boy with Molly walked across the street."
>>
>> Sentence (A) is longest, but it makes perfect sense.
>>
>> Sentence (B) is nonsensical and grammatically incorrect because while the
>> word "with" takes two arguments, only one is present. If there was a comma
>> after "dog," then it would make sense; "With the dog, Molly walked across
>> the street" is fine. But if we were to assume that Molly is not the dog, in
>> this case, we'd actually be wrong.
>>
>> Sentence (C), while grammatically correct, leaves it unclear whether
>> Molly is a dog, a girl, or… something else.
>>
>> The reason for this is, whenever a preposition is used in English, it
>> almost always takes a dyadic form, relating a subject to the preposition's
>> object. The two most common dyadic formats are:
>>
>> *<subject> [<preposition> <object of preposition>]*
>> <The boy> [<with> <the dog>] crossed the street.
>>
>> *[<preposition> <** object of preposition**>] <subject>*
>> [<In> <space>], <no one> can hear you scream.
>> [<On> <the Moon>] are <many craters>.
>>
>> Now, in Objective C through Swift 1 and 2, prepositions' dyadic nature
>> were generally respected in method signatures. However, Swift 3's migration
>> of the preposition inside the parentheses also seems to have been
>> accompanied by the stripping away of either the subject, the prepositional
>> object, or both—according to no discernible pattern. For example:
>>
>> (1) CloudKit:
>>
>> old: myCKDatabase.fetchRecordWithID(recordID)
>> new: myCKDatabase.fetch(withRecordID: recordID)
>> *(subject "Record" got removed)*
>>
>>
>> (2) String:
>>
>> old: myString.capitalizedStringWithLocale(_: myLocale)
>> new: myString.capitalized(with: myLocale)
>> *(subject "String" and prep. object "Locale" both got removed)*
>>
>>
>> (3) Dictionary:
>>
>> old: myDict.removeAtIndex(myIndex)
>> new: myDict.remove(at: myIndex)
>> *(subject "element" already missing from both; prep. object "Index" got
>> removed)*
>>
>>
>> (4) Date:
>>
>> old: myDate.timeIntervalSinceDate(myDate)
>> new: myDate.timeIntervalSince(date: myDate)
>> *(subject "timeInterval" and object "Date" both still present; but oddly,
>> preposition "since" got left outside of the parentheses)*
>>
>> (5) Array:
>>
>>     old: myArray.forEach({ thing in code})
>>
>> new: myArray.forEach() { thing in //code }
>>
>>             *(preposition “for” is outside of the parentheses)*
>>
>> *DISCUSSION OF EXAMPLES*
>>
>> These four changes are inconsistent with each other regarding whether the
>> subject, prepositional object, or both were removed, and we don't even have
>> consistency as to whether the preposition got moved inside the parentheses
>> (see A below).
>>
>> As well, these changes generally removed the dyadic arguments to the
>> preposition, which removes cues necessary to disambiguate the prepositional
>> relationship, decreasing code readability (see B below).
>>
>> *(A) Inconsistency*
>>
>> The inconsistency between the examples is shown in the bold text of each
>> example, but lets go over why this matters. It matters because any language
>> is easier to learn the more consistently it sticks to its own rules.
>> Autocomplete is our great savior, but still, if we were being consistent,
>> then the new method signatures would have been:
>>
>> (1) myCKDatabase.fetchRecord(withRecordID:)
>> (2) myString.stringCapitalized(withLocale:)
>> (3) myDictionary.elementRemoved(atIndex:)
>> (4) myDate.timeInterval(sinceDate:)
>> (5) myArray.each(inClosure: )
>>
>> Side note: for plain English readability, we might prefer
>> elementRemoved(fromIndex:) and stringCapitlized(accordingToLocale:).
>>
>> Although I do understand removing "string" from the latter was to reduce
>> redundancy in function/method declarations, we only make one declaration,
>> yet we make many calls. So increasing ambiguity in calls does not seem like
>> a good trade-off for decreased boilerplate in declarations. More often than
>> not it's calls that we're reading, not the declarations—unless of course
>> the call was ambiguous and we had to read the declaration to make sense out
>> of it. So perhaps we might question if increased ambiguity is an overall
>> good thing.
>>
>> Side note: example (5), .forEach, does seem like a very exceptional case,
>> and possibly a sacred cow. See the Proposal section for further discussion
>> of this.
>>
>> *(B) Increased Ambiguity*
>>
>> In all of these changes, one of or both parts of the dyadic arguments of
>> the preposition have been excised from the method signatures. This
>> increases ambiguity in Swift 3/4 vs. Objective C and Swift 2, especially in
>> closures, as I will explain below.
>>
>> In example (1), the old method argument makes grammatical sense because
>> "record with RecordID" follows the format, *<subject> [<preposition>
>> <object of preposition>]*, just like "boy with dog tag" or "cow with
>> black spots." This improves code readability, because when you read this,
>> you know that this function will give you a record matching a particular
>> recordID. However in Swift 3, we have the equivalent of, "with a
>> recordID"—i.e. is *implied* that the thing being fetched is a record.
>>
>> This isn't a problem you're reading the method signature in the header,
>> and it's not a problem when you're writing the code, because you'll get
>> warnings and errors if you try to assign to the wrong type.
>>
>> However this removal of explicit contextual cues from the method
>> signature harms readability, since now, the compiler will let people write
>> code like:
>>
>> { return $0.fetch(withRecordID:$1) }
>>
>> Clearly, the onus is now on the developer not to use cryptic, short
>> variable names or NO variable names. However, spend much time on GitHub or
>> in CocoaPods and you will see an increasing number of codebases where
>> that's exactly what they do, especially in closures.
>>
>> Another problem is that the compiler doesn't care if you write:
>>
>> { ambiguousName in
>> let myRecordID = ambiguousName.fetch(withRecordID:myID)
>> return myRecordID }
>>
>> This is highly problematic because someone reading this code will have no
>> reason to expect the type of "myRecordID" not to be CKRecordID. (In fact,
>> it's CKRecord.) There is also no way to clarify what ambiguousName is since
>> closures don't have argument labels at all... but that's another problem
>> altogether.
>>
>> Turning now to example (2), "myString.capitalized(with:myLocale)"
>> sacrifices BOTH the subject and the prepositional object. We have now lost
>> any enforced contextual cues, fully orphaning "with", allowing and even
>> encouraging a final closure argument like:
>>
>> { return $0.capitalized(with: .current)) }
>>
>> What is $0? Reading this, and being familiar with Swift, you will likely
>> assume it's a string, but will you remember that .current is a Locale? The
>> compiler knows, but why was it a good idea to strip out that information?
>>
>> We also have examples like:
>>
>> { return $0.draw(with:$1) }
>>
>> What is $0? What is $1? This is a real Apple API, BTW.
>>
>> We could also have:
>>
>> {array, key in
>> let number = array.remove(at:key)
>> return number }
>>
>> This will compile and run even though number will be a tuple key-value
>> pair, array will be a dict, and key will be an index type! This may seem
>> like a ridiculous example, but I have literally seen things like this.
>>
>> *DISCUSSION*
>>
>> Making computer code more like natural language and thus more
>> approachable to non-computer-science types was clearly one of the primary
>> goals of Apple's method names in Objective C.
>>
>> I've been a fan of the general direction in which Swift has taken things,
>> but I'm starting to see code that is quite unreadable as a result of the
>> stripping away of useful information from code, as if more white space will
>> improve our ability to make sense out of our arcane invocations.
>>
>> The point of code readability is for humans to be able to read and
>> understand code with minimal extra effort. Explicit information is
>> important because humans would like to read code without having to dig
>> through five different headers and read lots of documentation files. This
>> directly increases costs, because when a new developer comes onto a
>> project, the longer it takes them to understand the codebase, the more it
>> will cost the company.
>>
>> I don't believe the intent with these changes was to increase ambiguity;
>> at WWDC '16 we were told it was to increase clarity. I'm just not sure
>> that's the consistent effect we actually received.
>>
>> What we got instead is more times we will need to CMD-click things in
>> XCode to get a clue. That's especially annoying because you're at XCode's
>> mercy as to whether it actually wants to jump to a given declaration or
>> not. This seems pretty hit or miss in my experience, even within a basic
>> playground page, and is especially troublesome with jumping to declarations
>> within CocoaPods. Selecting some code to get documentation in the side
>> panel seems equally unreliable.
>>
>> Perhaps CMD-click problems have to do with a project setting, but is
>> there really a time when you would ever NOT want to have these
>> capabilities? Why is it tied into a setting? Further, when you're reading
>> through a repo on GitHub itself, you don't have CMD-click capability; you
>> want the code to be as clear as possible, without lots of ambiguity.
>>
>> *From Ambiguous to Cryptic*
>>
>> Orphaning method signatures by stripping useful return type and argument
>> type information wouldn't be so bad if variables were all named
>> descriptively, but that is a strangely optimistic hope for a language
>> that's as paranoid about safety that it was specifically designed to
>> prevent many categories of common mistakes.
>>
>> The increased ambiguity in Swift from the removal of prepositional
>> components has been further compounded by other forms of ambiguity that are
>> increasingly everywhere in Swift, and which developers have taken copious
>> advantage of to the point of abuse. Examples are:
>>
>> - arguments labelled as "_" reduce code clarity
>> - calls to functions that take closures as the final argument can leave
>> the final argument label out, decreasing readability
>> - closures have lack explicit type information, leaving it up to variable
>> names to let us know what's going on; however, the use $# syntax and/or
>> cryptically named variables often leaves it totally ambiguous
>> - generic types in method signatures using a single letter give no clue
>> as to the type's purpose
>> - function types can't have argument labels, which might give a sense of
>> context or informative cues, even if they added boilerplate
>> - inferred nested type parents (like the ability to do .current instead
>> of being forced to do Location.current)
>>
>> *Why is removing context and clue good?*
>>
>> Is it possible that this was an overreaction to the perceived
>> over-verbosity of Objective C? I'm honestly curious. I will try to read
>> back through the archives, but perhaps someone can give me a good summary.
>>
>> In the old Objective C style, method names could be quite long, but I
>> never felt a lack of context that made code unclear. The method signatures
>> were basically like sentences without spaces, and they seemed to follow
>> pretty reliable rules. So reading them actually made grammatical sense,
>> improving code readability. I could always tell what a call was doing.
>>
>> While I won't deny that Objective C went overboard sometimes in
>> verbosity, and while few would dispute that we should clean up its style,
>> could it be we are going too far making Swift encourage code that's
>> ambiguous to everyone except the compiler? Could it be a mistake for Swift
>> to err so far on the side of inferring everything and stripping away
>> contextual information?
>>
>> A first-time reader of some unseen code is not going to be able to infer
>> things like the original programmer or the compiler can. This increases the
>> possibility of bugs (and exploits) sneaking in "under the radar."
>>
>> *PROPOSAL*
>>
>> To add more clarity back to Swift, I'd propose the following rules be
>> applied consistently across the language:
>>
>> • Every time a preposition shows up in a function/method signature, there
>> is an explicit subject and object of the preposition, except in the
>> following exceptions.
>>
>> • The subject of a preposition can be left out if the function/method is
>> mutating AND doesn't return a value, such as Array's .append(contentsOf:)
>> method. This method would become: .append(contentsOfCollection:) so that
>> it's clear what we're doing here, even within a closure.
>>
>> • No preposition should be used when it would add no extra meaning
>> between what’s inside and outside the parentheses. For example,
>> userFont(ofSize fontSize:) should just be userFont(size: fontSize), because
>> “of” is completely superfluous here. AVFragmentedMovie’s
>> track(withTrackID:) could just as easily be track(forTrackID:) or
>> track(TrackID:) which tells us that the preposition is superfluous and so
>> why not just have it be track(id:).
>>
>> • If the preposition goes with a verb in the method signature, the verb
>> should be the last thing before the opening parenthesis (e.g. String's
>> .capitalized function would now look like name.stringCapitalized(accordingToLocale:
>> .current)... no pun intended).
>>
>> • In the case of .forEach, since it’s almost identical in function to
>> .map, perhaps a solution could be to eliminate .forEach entirely, and
>> simply say that anytime .map’s result is unused (or assigned to _), then
>> the compiler will automatically use .forEach functionality to optimize
>> performance. For example if in Swift 5 you did _ = myArray.map() {
>> print($0) }, this would be the same as doing myArray.forEach() { print($0)
>> } currently. As well, _ = could be inferred.
>>
>> Note: part of the problem is obviously that parameter names are never
>> used in function calls when there is an argument label, but often, the
>> argument label is now just a preposition, while its prepositional object is
>> the (now hidden) parameter name. Perhaps we can think of a novel solution
>> to this?
>>
>> I'm interested to know what people think about this. Perhaps there are
>> other ideas or ways to add clarity back in. Thanks for reading this long
>> message.
>>
>> Jon
>> _______________________________________________
>> 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
>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170802/183d9c3c/attachment.html>


More information about the swift-evolution mailing list