- Proposal: SE-0059
- Author: Dave Abrahams
- Status: Accepted for Swift 3 (Rationale)
- Review manager: Doug Gregor
When
SE-0006, Apply API Guidelines to the Standard Library
was proposed, the lack of an acceptable naming convention for some
mutating/nonmutating method pairs meant that the APIs of SetAlgebra
,
Set<T>
and OptionSet<T>
were not adjusted accordingly. This
proposal remedies both problems by:
-
establishing the necessary naming conventions and
-
applying the corresponding changes to the Set APIs. A few other issues in these APIs are cleaned up along the way (details below).
For reference as you read this proposal, you may be interested in the following links:
- Updated API guidelines (corresponding diff)
- Updated
SetAlgebra
API (corresponding diff)
The guidelines say that method calls with side-effects should read
as verb phrases, and those without side-effects should read as noun
phrases. They also describe how to name mutating/nonmutating method
pairs accordingly: starting with the assumption that the fundamental
operation can be described by a verb, we are to use the ed
or ing
suffix to create a noun phrase for the nonmutating operation.
The problem is that in some cases, the operation's only natural
description is as a noun. Consider the union of two sets, or the
remainder when dividing two integers. In these cases, we have a
suitable name for the non-mutating operation, and we need to create a
name that reads as a verb phrase for its mutating counterpart. The
proposed solution is to use the form
prefix, so that
x.formUnion(y)
is equivalent to
x = x.union(y)
This proposal changes APIs of the SetAlgebra
protocol, which
propagate into the library's models of those protocols, Set<T>
and
OptionSet<T>
. Most of the changes amount to a straightforward
application of the new guidelines.
x = y.union(z)
y.formUnion(z) // y = y.union(z)
x = y.intersection(z)
y.formIntersection(z) // y = y.intersection(z)
x = y.subtracting(z)
y.subtract(z) // y = y.subtracting(z)
x = y.symmetricDifference(z)
y.formSymmetricDifference(z) // y = y.symmetricDifference(z)
if x.contains(c) { ... }
y.insert(a)
y.remove(b)
y.update(with: c)
if x.isSubset(of: y)
&& y.isStrictSubset(of: z)
&& z.isDisjoint(with: x)
&& y.isSuperset(of: z)
&& x.isStrictSuperset(of: z)
&& !y.isEmpty { ... }
There are a few notable changes to SetAlgebra
that go beyond simple
renaming:
-
The concept of elements subsuming or being disjoint with other elements has been dropped from the documentation, along with the corresponding static methods of
SetAlgebra
. The idea was only used in describing the semantics of theremove
method, but we have found a simpler way to describe those semantics. -
The semantics of
remove
's return value have changed slightly, to make them more useful forOptionSet
s. Whene
is a “compound option” with several bits set in itsrawValue
, and option sets
has a strict subset of those bits set in its raw value,s.remove(e)
no longer returnsnil
. Instead, it returnss.intersection(e)
. This change only affectsOptionSet
, notSet
. -
The semantics of
someSet.insert(newMember)
method have been changed slightly, so that ifnewMember
was already a member ofsomeSet
, it has no effect. This change in behavior is unnoticeable under most circumstances, but can be observed if equalElement
instances can be distinguished. For example, whenElement
is a class, instances may be distinguished using the===
operator. The new behavior matches that ofNSMutableSet.insert
, and is also likely to be more efficient. Users needing the old behavior can always use the newupdate(with:)
method, described below. In practice this change only affectsSet
, notOptionSet
. -
someSet.insert(newMember)
now returns a (discardable) pair containing an indication of whether the insertion took place and theElement
equal tonewMember
that is a member of the set after the insertion. This change is an expression of the principle that the library shouldn't discard potentially useful and information that may have a non-trivial cost to compute. -
A new
update(with: newMember)
API was added, to provide the previous unconditional insertion semantics of theinsert
API.
You can follow
this link
to see exactly how SetAlgebra
has changed. As noted earlier, all
other API changes proposed here are a consequence of applying exactly
the same changes to Set
and OptionSet
.
Like all renamings, this is a source-breaking change that can be largely automated by a migrator.
To avoid any semantic change one could consider automatically
migrating uses of someSet.insert(newMember)
to someSet.update(with: newMember)
, though the chances that a user actually wants the
semantics of update(with:_)
where she has used insert(_)
seem
quite slim.
The slight change to the result of remove
is unlikely to affect
anyone, but one could consider issuing a warning during migration to
inspect the usage if the returned value from someOptionSet.remove(x)
is not discarded.
So many alternatives to the form
prefix convention were considered
that it's impossible to enumerate them all, but only one candidate
stands out as being particularly worthy of mention: the InPlace
suffix that was previously used in the standard library. InPlace
has one major advantage over form
: the fact that it is a suffix
benefits grouping in alphabetical catalogs of method names and tools
that do code completion by prefix. However, the InPlace
suffix has
a few major weaknesses:
-
Reading someNoun
InPlace
as a verb phrase requires reading someNoun as a verb. A willingness to pretend that nouns are verbs undermines some basic principles of the API guidelines, which prescribe different uses for different parts of speech. -
InPlace
could more grammatically be applied to a verb, which means you'd really need to read the guidelines carefully to understand how to use it properly. A knowledge of common English doesn't lead toward properly applying it. -
InPlace
is visually heavyweight when compared toform
, and quite distasteful to some.
Since operation nouns tend to arise in mathematical domains, we considered avoiding math terms and instead using a more “container-like” API for sets:
x = y.insertingContents(of: z)
y.insertContents(of: z)
x = y.removingContents(notInCommonWith: z)
y.removeContents(notInCommonWith: z)
x = y.removingContents(inCommonWith: z)
y.removeContents(inCommonWith: z)
x = y.insertingContents(removingCommonContents: z)
y.insertContents(removingCommonContents: z)
if x.contains(c) { ... }
y.insert(a)
y.remove(b)
if x.allContentsAreContained(in: y)
&& y.allContentsAndMoreAreContained(in: z)
&& z.hasNoContentsInCommon(with: x)
&& y.containsAllContents(of: z)
&& x.containsAllContentsAndMore(of: z)
&& !y.isEmpty { ... }
Aside from the obvious awkwardness of some of the resulting code, we
felt that the loss of the immediately-recognizable semantics of
terms-of-art such as union
and intersection
was too great a cost.
We also considered being much more explicit about the semantics of the
insert(_)
/ remove(_)
/ update(with:)
suite of methods, leading
to usage like:
s.insertIfAbsent(x)
s.removeIfPresent(x)
s.insert(replacingIfPresent: x)
In the end, we thought, the extra words would not add clarity to typical uses of these APIs, where equal set elements are treated as indistinguishable.