If you want to use Objective-C you're free to use Objective-C, or more
generally any of the wonderful languages that choose a different tradeoff
regarding convenience over expressiveness. Otherwise, I'm not really sure
what productive forward movement bringing up complaints about a fundamental
philosophical underpinning of Swift that's not realistically going to
change is supposed to achieve. As was mentioned in a message earlier this
week, swift-evolution is a list to discuss making changes to the Swift
language, not a list for ranting about things in Swift you don't like but
cannot change.
Regards,
Austin
On Aug 2, 2017, at 11:43 PM, Goffredo Marocchi via swift-evolution < >>>> swift-evolution@swift.org> wrote:
Sent from my iPhone
On 3 Aug 2017, at 04:39, Brent Royal-Gordon via swift-evolution < >>>> swift-evolution@swift.org> wrote:
On Aug 2, 2017, at 10:49 AM, Xiaodi Wu via swift-evolution < >>>> swift-evolution@swift.org> wrote:
Brent had a draft proposal to revise the names of collection methods to
improve the situation here. There is room for improvement.
It didn't change `remove(at:)`, though. (Well, it might have affected
its return value or something, I don't remember.) It mostly addressed the
`dropLast()`, `prefix(_:)` etc. calls.
To respond to the original post:
Some of the APIs you cite are not very well named, but I think your
diagnosis is incorrect and so your prescription isn't going to help.
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)*
The thing is, the subjects and prepositional objects *are* present in
all of these—they are the parameters and targets of the calls.
In your own example, you say "In space, no one can hear you scream",
not "In location space, group-of-people no one can hear you scream". So why
is it a problem that we say "myString, capitalized with myLocale" instead
of "myString, string capitalized with locale myLocale"? These are
redundancies that we would never tolerate in natural language; I don't see
why you think code should be different.
(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)*
This is actually inaccurate—the parameter to `timeIntervalSince(_:)` is
unlabeled, so it's:
new: myDate.timeIntervalSince(myDate)
(5) Array:
old: myArray.forEach({ thing in code})
new: myArray.forEach() { thing in //code }
*(preposition “for” is outside of the parentheses)*
Yes, because the preposition does not apply to the parameter—it applies
to the operation as a whole. I'll have more to say on this in a moment.
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.
This is true, but you aren't just proposing sticking more closely to
our existing standards—you're proposing *changing* our standards. And I
don't think the changes you propose are an improvement. In fact, I'd say
each of these examples is worse:
(1) myCKDatabase.fetchRecord(withRecordID:)
"Fetch record with record ID"? I mean, you could at least remove the
`Record` before `ID`. What other ID do you suppose it would be?
I *can* see the case for going back to `fetchRecord` instead of just
`fetch`, though. On the other hand, I do think that, if you know it's a
database, the most obvious thing for `fetch` to be fetching is a record
from that database. It's not a dog—it won't be fetching a stick.
(2) myString.stringCapitalized(withLocale:)
Let's translate this to an actual use site, which is what we care about.
func tableView(_: UITableView, titleForHeaderInSection section: Int) ->
String? {
return sections[section].title.stringCapitalized(withLocale: .current)
}
What is `string` contributing here? We already know it's a "title",
which sounds a lot like a string. If you asked for a "capitalized" string,
what else would you get back if not another string?
The locale parameter is a little more tricky. You're right that `(with:
.current)` is vague, but I can imagine plenty of call sites where `with`
wouldn't be:
title.capitalized(with: german)
title.capitalized(with: docLocale)
title.capitalized(with: otherUser.locale)
Something at the call site needs to imply this is a locale, and there's
nothing in `(with: .current)` that does so. This is arguably just a style
issue, though: even though the language allows you to say `(with:
.current)`, you really ought to say `(with: Locale.current)` to be clearer.
Or perhaps the problem is with the name `current`—it ought to be
`currentLocale` (even though that's redundant when you write it out as
`Locale.currentLocale`), or it should use some location-ish terminology
like `home` or `native`.
(Actually, I think there might be a new guideline there: Variable and
property names should at least hint at the type of the value they contain.
Names like `current` or `shared` or `default` are too vague, and should
usually be paired with a word that implies the type.)
It might also help to change the `with` preposition to `in`, which
would at least imply that the parameter is related to some kind of location.
title.capitalized(in: german)
title.capitalized(in: docLocale)
title.capitalized(in: otherUser.locale)
title.capitalized(in: .current) // Still not great, but better
(3) myDictionary.elementRemoved(atIndex:)
This naming is exactly backwards, and is a perfect example of why we
*don't* want rigid consistency:
1. It emphasizes the element being returned, while making the "Removed"
operation an afterthought, even though the removal is the main thing you
want to happen and the element is returned as an afterthought.
2. It mentions the "Index", even though there is no other plausible
thing that could be labeled "at". (The only other plausible parameters to a
`remove` method are an element, an array of elements, a predicate, or a
range of indices. Of those four, only the range of indices could possibly
make sense with "at", but that ambiguity is a harmless overloading.)
Again, think about a use site:
func tableView(_: UITableView, commit edit: UITableViewCellEditingStyle,
forRowAt indexPath: IndexPath) {
assert(edit == .delete)
sections[indexPath.section].rows.elementRemoved(atIndex: indexPath.row)
// vs.
sections[indexPath.section].rows.remove(at: indexPath.row)
}
In context, `elementRemoved` obscures the purpose of the line, and
there is no ambiguity about what `at` means. The current name is *far*
better.
(4) myDate.timeInterval(sinceDate:)
I have a hard time thinking of a call site where `sinceDate` would be
an improvement over `since`; the preposition already *strongly* implies a
temporal aspect to the parameter.
If we remove `Date`, then in isolation I kind of like this change. The
problem arrives when you step out of isolation and think about the other
methods in its family. It would be strange to have
`myDate.timeInterval(since: otherDate)`, but `myDate.timeIntervalSinceNow()
`.
One solution would be to add `now`, `referenceDate`, and `unixEpoch`
(or `utc1970`) as static properties on `Date`. Then you could have just the
one `timeInterval(since:)` method:
myDate.timeInterval(since: otherDate)
myDate.timeInterval(since: .now)
myDate.timeInterval(since: .referenceDate)
myDate.timeInterval(since: .unixEpoch)
Another solution would be to add a `-` operator that takes two `Date`s
and returns a `TimeInterval`, sidestepping the wording issue entirely.
(5) myArray.each(inClosure: )
I don't get this name at all. This operation is completely imperative
and side-effectful, but there's no verb? How is it "in" the closure? Why
"closure" when you can pass a function, and in fact you probably will
*only* see this label if you're passing a function?
I do think there's a problem with `forEach(_:)`—it ought to be
`forEach(do:)`. This is much like `DispatchQueue.async(execute:)` or
the `withoutActuallyEscaping(_:do:)` function in the standard library.
When you pass a function parameter, and the call's primary purpose is to
run that parameter, it's often best to attach the verb to that parameter
instead of putting it in the base name. Of course, the complication there
is that the verb doesn't actually get written if you use trailing closure
syntax, but the curlies sort of fill that role. Kind of.
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.
I think you misunderstand the current Guidelines' goals. The Guidelines
are not trying to reduce redundancy at declaration sites—they're trying to
reduce redundancy at *call sites*. The idea is that, if the variable names
for the method's target and parameters imply something about the types they
contain, those names along with the prepositions will imply the purpose of
each parameter, and therefore the call. The types are just a more formal
version of that check.
That's why the very first paragraph of the API Design Guidelines <
https://swift.org/documentation/api-design-guidelines/> says:
Clarity at the point of use is your most important goal. Entities such
as methods and properties are declared only once but used repeatedly.
Design APIs to make those uses clear and concise. When evaluating a design,
reading a declaration is seldom sufficient; always examine a use case to
make sure it looks clear in context.
So the tradeoff is not between *declaration redundancy* and call site
clarity—it is between *call site redundancy* and call site ambiguity.
Because their parameters are unlabeled, most languages have severe call
site ambiguity problems. Objective-C has a pretty serious call site
redundancy problem. Swift's design is trying to hit the baby bear "just
right" point.
It is quite possible that, in some areas, we have swung too far back
towards ambiguity. But I don't think `elementRemoved(atIndex:)` is going to
fix that.
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.
What I think you're missing with this example—and in fact with all of
your closure-based examples—is that closures don't exist in isolation;
they're in some larger context. (Otherwise, they won't compile.) For
instance, the above closure might be in a line like:
return zip(databases, recordIDs)
.map { return $0.fetch(withRecordID:$1) }
Read in the context of its line, the meanings of $0 and $1 are fairly
clear.
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.)
Again, in the larger context, this line will end up generating a
`[CKRecord]` array instead of a `[CKRecordID]` array, which is probably
going to cause a type mismatch once you try to actually use the alleged
record IDs. (But as I said earlier, I can see the case for using
`fetchRecord(withID:)` or `fetchRecord(with:)` instead of
`fetch(withRecordID:)`.)
Ambiguous names can hide bugs in their ambiguity, but verbose names can
also hide bugs in the sheer mass of code they generate. The difference is,
developers can manage the ambiguity in their code by naming variables well,
but they can't manage verbosity if verbose names are imposed on them.
We also have examples like:
{ return $0.draw(with:$1) }
What is $0? What is $1? This is a real Apple API, BTW.
Again, the context of the closure would tell you, but part of the
problem here is that they held onto an Objective-C preposition which was
poorly chosen. If the line were `$0.draw(in:$1)`, you would know that
`$1` specified an area of the screen and `$0` was something that could be
drawn, which frankly is all you really *need* to know to understand what
this line does.
{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.
Where have you seen something like this? `array` would have to be
passed `inout` for this to work at all.
Nevertheless, how would more verbose names help with this problem? This
is every bit as incorrect, and the compiler will still accept it:
{array, key in
let number = array.elementRemoved(atIndex:key)
return number }
Are you thinking that they'll notice that `atIndex` is not `atKey`?
There is already a much stronger safeguard against that: `Dictionary.Index`
and `Dictionary.Key` are different, totally incompatible types. Every
mechanism I can think of to get a `Dictionary.Index` has "index" or
"indices" in its name, so you could only make this mistake if you confused
dictionary indices with dictionary keys, in which case `atIndex:` would not
stand out to you either.
Ultimately, unless the compiler actually understands identifiers, it's
just not going to be able to catch mistakes like calling a dictionary an
"array", or many of the other problems you describe here. But the type
system can and often does flag these kinds of problems pretty close to the
source.
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.
Personally, I think of Swift's approach to safety as similar to
Reagan's "trust, but verify". Our naming conventions trust the programmer
to write code with clear names; our picky type system verifies that the
code is plausible. We don't force the programmer to explain herself to us
until we notice that something doesn't seem right.
The bottom line is, a language can't force you to write clear and
correct code, but it has many tools it can use to encourage it. Swift
chooses not to use the "heavily redundant names" tool because its cost to
good code is too high. We instead rely more on other tools, like strong
typing, value types, and definite initialization, to encourage high-quality
code at a lower cost.
More verbose rather than heavily redundant names tool, but what have we
lost by being reliant on strongish static typing and value types? Would
Cocoa/ UIKit be so nice to use if rewritten fully in idiomatic non @objc
Swift (compared to working with UI on C++ platforms)? What would it tell us?
Sorry for the rant... do not take static typing laying down j/k ;).
(still quite in “love” with the opt-in security that annotations and clang
warnings bring in Objective-C rather than the opt-out freedom model some
other languages have)
--
Brent Royal-Gordon
Architechies
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution