Curious about what’s new in Swift 3? In this final part of a three-post series, Daniel Steinberg covers Clarity, Good Looking C, and API Guidelines.
You can find the other parts here:
- Part 1 - Enums and Parameters
- Part 2 - Functions & Closures, Collections, and Living with Guidelines
Clarity
Move where clause to end of declaration (Proposal 0081)
Many of the Swift 3 proposals revisited existing Swift code and said, “How can we make this easier for humans to parse this code?” For example, in this function anyCommonElements
, it takes two generic types:
func anyCommonElements<T : SequenceType, U : SequenceType where
T.Generator.Element: Equatable,
T.Generator.Element == U.Generator.Element>(lhs: T, _ rhs: U) -> Bool {
...
}
But the parameters for the function come way there at the end. There is an awful lot in between the declaration of the generic types and the declaration of the parameters in the return type for this function. In between is this where
clause that is important, but it is not as important as the parameters in the return type of the function.
In Swift 3, this where clause will move to the end, after the parameters and the return type of the function, and before the opening curly brace:
func anyCommonElements<T : SequenceType, U : SequenceType>(lhs: T, _ rhs: U) -> Bool
where
T.Generator.Element: Equatable,
T.Generator.Element == U.Generator.Element {
...
}
Restructuring condition clauses (Proposal 0099)
In guard
clauses and if
clauses, we could have multiple, comma-separated conditions. The rule so far has been: we could have a Boolean
expression followed by various let
s:
guard
x == 0,
let y = optional where z == 2
else {
In this example, we are testing that x
is zero, and we are binding y
where z
is two. We need to use this where
clause because we cannot put a Boolean
expression after the let
.
In Swift 3, we can comma-separate, and move the where
clause down as just a standard Boolean condition:
guard
x == 0,
let y = optional
z == 2
else {
And so regarding the x
is zero, we are binding y
, and regarding that z
is 2.
In Swift 2, this would be confusing because we do not have to repeat the let
for various bindings. In Swift 3, we cannot do something like this, where we guard let
, and we bind three things at the same time:
guard let x = opt1,
y = opt2,
z = opt3,
booleanAssertion else {}
x
is optional 1 and y
is optional 2, and z
is optional 3. Instead, in Swift 3, we must repeat the keyword let
:
guard let x = opt1,
let y = opt2,
let z = opt3,
booleanAssertion else {}
And although that makes it just a little bit wordier, it makes it clearer, and it allows us to intersperse let
s in Boolean
expressions.
The same is true of case
, we have to repeat the keyword case
, but again that adds flexibility to the condition clauses:
if case let x = a,
case let y = b {
Improved operator declarations (Proposal 0077)
Operator declarations have been a mess:
infix operator <> { precedence 100 associativity left }
There is nothing that says “100
goes with precedence
”, there is nothing that helps us parse this.
In Swift 3, we can create a precedencegroup
:
precedencegroup ComparativePrecedence {
associativity: left
higherThan: LogicalAndPrecedence
}
infix operator <> : ComparativePrecedence
In this case, we assign the associativity
to be left
, and notice the colon in between that says, That is the value of the associativity
. We set the precedence level to be higherThan
LogicalAndPrecedence
, we are comparing it with numbers like 100 and 120 as we did in the past. That is what makes up the definition of this ComparativePrecedence precedence group, and now we can declare our operator and specify that it has this associativity and precedence level.
Replacing equal signs with colons for attribute arguments (Proposal 0040)
You saw how much cleaner it was to use colons to map the associativity
and precedence
level, the same is going to be done in this proposal to attribute arguments that use =
:
introduced=version-number
deprecated=version-number
obsoleted=version-number
message=message
renamed=new-name
mutable_variant=method-name
The equals sign will be replaced by the colon:
introduced: version-number
deprecated: version-number
obsoleted: version-number
message: message
renamed: new-name
mutable_variant: method-name
This is less about one looking better than the other, and more about consistency across the APIs.
Clarify interaction between comments & operators (Proposal 0077)
What happens when you put a comment in place?
if/* comment */!foo { ... }
When you remove the comment, is there a white space between If and the exclamation point, or not? In Swift 3, a comment will always be equivalent to a white space:
if !foo { ... }
The comment is interpreted as if there was just a white space between the if
and what follows.
Tuple comparison operators (Proposal 0015)
If you have a tuple of comparables, why can you not compare the two tuples? In Swift 3 now you can, up to arity 6. The rule is simple: for equals (==
) and non-equals (!=
), you just compare element by element. For order comparisons, you do it in lexicographical order, starting with the first element and comparing; then, if you need a tiebreaker, going on to the next element.
Good Looking C
“Classic” CoreGraphics code (Proposal 0044)
When we connect to C code from Swift, it does not look very Swifty, and some attention has been paid to taking care of this.
For instance, let’s start with some classic core graphics code:
override func drawRect(rect: CGRect) {
let context: CGContext = UIGraphicsGetCurrentContext()!
let toCenter = CGPoint(x: bounds.width/2.0, y:bounds.height/2.0)
let angle = CGFloat(M_PI / 16)
var transform = CGAffineTransformIdentity
for _ in 0..<32 {
triangulateRect(bounds, inputTransform: transform, context: context)
transform = CGAffineTransformTranslate(transform, toCenter.x, toCenter.y)
transform = CGAffineTransformRotate(transform, angle)
transform = CGAffineTransformTranslate(transform, -toCenter.x, -toCenter.y)
}
CGContextSetLineWidth(context, bounds.size.width / 100)
CGContextSetGrayStrokeColor(context, 0.5, 1.0)
CGContextDrawPath(context, .Stroke)
}
In this case, we have got a drawRect method, and we create a context. Now when we do things like set the line width, we have to pass in the context
as one of the parameters, that is traditional C code. We do that when we set the line width, the stroke color, and when we actually draw the path.
Similarly, when we create an AffineTransform
, when we go to use that, we have to pass in the transform
as the first argument. We do it because transforming contexts or C constructs, and the functions that deal with them are just plain old C functions.
The great renaming (Proposal 0023)
There are many things we can do to change this code:
override func draw(_ rect: CGRect) {
let context: CGContext = UIGraphicsGetCurrentContext()!
let toCenter = CGPoint(x: bounds.width/2.0, y:bounds.height/2.0)
let angle = CGFloat(M_PI / 16)
var transform = CGAffineTransformIdentity
for _ in 0..<32 {
triangulateRect(bounds, inputTransform: transform, context: context)
transform = CGAffineTransformTranslate(transform, toCenter.x, toCenter.y)
transform = CGAffineTransformRotate(transform, angle)
transform = CGAffineTransformTranslate(transform, -toCenter.x, -toCenter.y)
}
CGContextSetLineWidth(context, bounds.size.width / 100)
CGContextSetGrayStrokeColor(context, 0.5, 1.0)
CGContextDrawPath(context, .Stroke)
}
To start with, there is The Great Renaming where drawRect
has been renamed. There is this redundancy where it is drawRect
and then we pass in a Rect
, so drawRect
just becomes draw
. And similarly, we do not have to say “draw” what, and then pass it in a Rect
, and we put in this _
. And when the system calls this method, it is called as draw
, and passes in the Rect
to be drawn.
Import as Member (Proposal 0044)
The magic of changing the context and the transform and how they are used comes with this proposal to import as member. We take these C structures and these functions, and we specify, when we bring these into Swift, that we import them as if they’re a member of a structed self.
Of course, in Swift, structs can have behavior. When we transform our C API’s into Swift, we want to treat these structs as if they have this behavior. Instead of having to pass in context and transform, it should be context.
and transform
.
override func draw(_ rect: CGRect) {
...
context.setLineWidth(bounds.size.width / 100)
context.setGrayStrokeColor(0.5, 1.0)
context.drawPath(.Stroke)
}
Instead of passing context in as the first parameter for ContextSetLineWidth
, it is now context.setLineWidth
. Similarly, context comes out front for the next two methods; we do not pass in as the first parameter in our function call. Instead, it becomes context.
. And then while we are at it, we do clean these up a bit:
override func draw(_ rect: CGRect) {
...
context.setLineWidth(bounds.size.width / 100)
context.setStrokeColor(gray: 0.5, alpha: 1.0)
context.drawPath(using: .stroke)
}
setGrayStrokeColor
becomes setStrokeColor
and then gray
as the label for the first parameter, and drawPath
gets the label using
and .stroke
goes to lowercase.
Similarly, for transform
, we are going to change all these times that we use transform
, and it is going to be transform.
:
override func draw(_ rect: CGRect) {
...
var transform = CGAffineTransform.identity
for _ in 0..<32 {
triangulateRect(bounds, inputTransform: transform, context: context)
transform = transform.translateBy(x: toCenter.x, y: toCenter.y)
.rotate(angle)
.translateBy(x: -toCenter.x, y: -toCenter.y)
}
...
}
What is used to create these import as members is specified in the NS_SWIFT_NAME
which has been around for a while, but is now being used in much deeper ways to make the imported APIs more Swifty.
Modernize libdispatch for Swift 3 naming conventions (Proposal 0088)
Outside of this general work being done on C-based APIs is an additional proposal for modernizing libdispatch for Swift 3. Here we create a queue using a typical C way of doing things, dispatch_queue_create
, and then pass in the arguments:
let queue = dispatch_queue_create("com.test.myqueue", nil)
dispatch_async(queue) {
print("Hello World")
}
That does not look very Swifty; instead, we are going to create a queue using something that looks much more like a constructor for dispatch_queue
:
let queue = DispatchQueue(label: "com.test.myqueue")
dispatch_async(queue) {
print("Hello World")
}
Similarly, we are going to use it instead of making this call to dispatch_async
and passing in queue. The dispatch queue class is going to include the synch
and the asynch
methods, and we will call these by just using queue.synch and
queue.asynch`:
class DispatchQueue : DispatchObject {
func synch(execute block: @noescape () -> Void)
func asynch(
group: DispatchGroup? = nil,
qos: DispatchQoS = .unspecified,
flags: DispatchWorkItemFlags = [],
work: @convention(block) () -> Void)
}
API Guidelines
API Design Guidelines (Proposal 0023)
Of all of the Swift proposals, the API Design Guidelines is the most important and informs many of the others. Some of the advice is general, and some of it is very specific. For example, one of the overall goals for an API is that there should be clarity at the point of use. There is an emphasis on the simplicity and the clarity at the call site as opposed to laboring over the method signature itself.
Although Swift is a more concise language than Objective-C, one of the important guidelines is that clarity is more important than brevity; if you have to lengthen a method name or add another label to make use of the method clear, you should do so.
Naming - Promote Clear Usage
There are various categories in the design guidelines, the first of which is naming. One of your guiding principles for naming should be to promote clear usage. This will require that you include all the words needed to avoid ambiguity. For example, here we have employees
remove something:
employees.remove(x)
employees
is an array, and what do we mean by removes something? Is the “something” the item to be removed? In this case, x
stands for the index of the array this API becomes clearer if we instead say:
employees.remove(at: x)
Omit needless words
The first principle is to add in words for clarity; the second principle is to omit needless words:
allViews.removeElement(view: cancelButton)
In this example, allViews
is a collection of views and when we removeElement
, we do not have to specify that the element we are removing is a view - all of the elements are views. We can remove the label view:
and still be completely clear. We could go further and remove the word element
:
allViews.remove(cancelButton)
It almost reads like an English phrase and is completely descriptive.
Name variables, parameters, and associated types according to their roles
class ProductionLine {
func restockFrom(widgetFactory: WidgetFactory)
}
In this example, this might be clearer if you think of it from the call site as restock
as the name of the method and we move from
inside:
class ProductionLine {
func restock(from widgetFactory: WidgetFactory)
}
widgetFactory
is the internal label - we use it within the function; even though we only use widgetFactory
inside of the function, this is an odd choice for an internal label. Perhaps, instead of “widgetFactory” that is too specific, and we want to say “supplier.”:
class ProductionLine {
func restock(from supplier: WidgetFactory)
}
We are restocking from whatever the supplier is and in this case, the supplier is a WidgetFactory
.
Precede each weakly typed parameter with a noun describing its role
func add(_ observer: NSObject, for keyPath: String) {}
grid.add(self, for: graphics)
In the “add” function, we have an observer
, and we have a keyPath
. The observer does not have an external label and the keyPath has the external label for
. When we call this it is not clear what self
refers to, or what graphics
refers to. Instead, we add a noun describing the role, and we can do that to the method name or to the label, and we end up with:
func addObserver(_ observer: NSObject, forKeyPath path: String) {}
grid.addObserver(self, forKeyPath: graphics)
Naming - Strive for Fluent Usage
Those are some guidelines for promoting clear usage we also strive for fluent usage.
Prefer method and function names that make use sites from grammatical English phrases
This example looks like very traditional code:
x.insert(y, position: z)
But that is not the way we speak. We would be more likely to say:
x.insert(y, at: z)
And that is the API that we prefer.
The following are some of the most controversial parts of these API design guidelines.
Naming functions and methods for their side effects
Functions or methods without side effects should read as noun phrases.:
x.distance(to: y)
i.successor()
Those that have side effects should read as imperative verb phrases:
print(x)
x.sort()
x.append(y)
Mutating/Non-Mutating pairs
Here was the controversial part: sometimes we have mutating and non-mutating parts; and when the operation is naturally described by a verb, we use the ed/ing ending for non-mutating:
x.sort()
z = x.sorted()
x.append(y)
z = x.appending(y)
The partner to “sort” and to “append” are “sorted” and “appending”. x.sort()
is the mutating version z = x.sorted()
is the non-mutating; x.append()
is the mutating version, z = x.appending()
is the non-mutating.
This principle caused problems with some of the terms, and a second principle was added: when the operation is naturally described by a noun, use form for mutating:
x = y.union(z)
y.formUnion(z)
j = c.successor(i)
c.formSuccessor(&i)
This went a long way to making people feel better about the earlier principle.
Update API naming guidelines and rewrite set APIs accordingly (Proposal 0059)
The examination of union
and that whole discussion led to a sister proposal for updating the API Naming Guidelines for Set APIs.
x = y.union(z)
y.formUnion(z)
Now back to the API Design Guidelines.
API Design Guidelines (Proposal 0023)
Now that we have protocols extensions, we can prefer methods and properties to free functions. Before protocol extensions, we were stuck using free functions because we did not know where to park that functionality.
When you are naming the various parts of a function or method, you should chose parameter names to serve as documentation:
func move(from start: Point, to end: Point)
Here the external name is move from
one place to
another place. Internally it is start
and end
as you are working with these points, internal to the function - you know which one refers to the start, and which one refers to the end.
In Swift, we can have default values for parameters. Many people forget that we can do this in init
s as well. Instead of having a whole family of init
s with different numbers of parameters, we can have default values for parameters, and have a smaller set of init
s.
It is not a requirement in Swift that we locate the parameters with defaults towards the end, but it is a preference; we prefer to put them towards the end of the parameter list.
Apply API Guidelines to the Standard Library (Proposal 0006)
In light of these new API design guidelines, it is important to go back to the Swift standard library and make sure that the Swift standard library conforms to these guidelines. It was essential that those writing the importers for the Objective-C APIs made sure that the Swift versions conformed to these API design guidelines.
Resources
- Proposal 0006 - Apply API Guidelines to the Standard Library
- Proposal 0015 - Tuple comparison operators
- Proposal 0023 - API Design Guidelines
- Proposal 0040 - Replacing Equal Signs with Colons For Attribute Arguments
- Proposal 0044 - Import as member
- Proposal 0059 - Update API Naming Guidelines and Rewrite Set APIs Accordingly
- Proposal 0077 - Improved operator declarations
- Proposal 0081 - Move
where
clause to end of declaration - Proposal 0088 - Modernize libdispatch for Swift 3 naming conventions
- Proposal 0099 - Restructuring Condition Clauses
Receive news and updates from Realm straight to your inbox