On Wed, Aug 2, 2017 at 2:23 PM, Robert Bennett <[email protected]> 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 <[email protected]> wrote: > > > On Wed, Aug 2, 2017 at 12:00 Robert Bennett via swift-evolution < > [email protected]> 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 < >> [email protected]> 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 >> [email protected] >> https://lists.swift.org/mailman/listinfo/swift-evolution >> >> >> _______________________________________________ >> swift-evolution mailing list >> [email protected] >> https://lists.swift.org/mailman/listinfo/swift-evolution > > >
_______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
