(How to Avoid) Seven Swift Snares

Update: As of 4/28/16, I have rechecked all the snares and they all remain. The currying-related snare will be going away, as the currying syntax has been deprecated.

Swift is accomplishing an amazing feat; it is transforming the way we think about programming for Apple devices, bringing in more modern paradigms such as functional programming and richer type-checking than the Smalltalk-inspired pure object-oriented model of Objective-C.

The Swift language is intended to help developers avoid bugs by adopting safe programming patterns. Inevitably though, such an ambitious undertaking will produce an artifact that (for now, at least) has a few rough edges, snares that can introduce bugs into programs without any warnings from the compiler. Some of these are mentioned in the Swift book, and some (as far as I can tell) are not. Here are seven snares most of which have caught me in the past year. They involve Swift’s protocol extensions, optional chaining, and functional programming.


Protocol extensions: powerful, but use with caution

The ability for one Swift class to inherit from another is a powerful weapon in the programmer’s arsenal because it makes specialization relationships explicit and supports fine-grained code sharing. But, unlike Swift’s reference types, its value types (i.e. structures and enumerations) cannot inherit from each other. However, a value type can inherit from a protocol, which in turn can inherit from another protocol. Although a protocol cannot contain code, only type information, a protocol extension can contain code. In this fashion, code can be shared by creating an hierarchy whose leaves are value types, and whose interior and root nodes are protocols with their corresponding extensions.

But Swift’s implementation of protocol extensions, being somewhat new and unexplored territory, has a few issues. The code doesn’t always do what one might expect. Since the snares in Swift involve the structure and enumeration value types in combination with protocols, we’ll introduce our example with classes to illustrate it without the snares. When it is recast into value types and protocols there will be a few surprises.

Introducing the example: classy pizza

Suppose there are three kinds of pizza made with two kinds of grain:

enum Grain  { case Wheat, Corn }
class  NewYorkPizza  { let crustGrain: Grain = .Wheat }
class  ChicagoPizza  { let crustGrain: Grain = .Wheat }
class CornmealPizza  { let crustGrain: Grain = .Corn  }

Each kind of pizza can respond to inquiries about its crust:

 NewYorkPizza().crustGrain 	// returns Wheat
 ChicagoPizza().crustGrain 	// returns Wheat
CornmealPizza().crustGrain 	// returns Corn

Since most pizzas are made with Wheat, the common code can be factored out into a default implementation embedded in a common superclass:

enum Grain { case Wheat, Corn }
class Pizza {
    var crustGrain: Grain { return .Wheat }
    // other common pizza behavior
}
class NewYorkPizza: Pizza {}
class ChicagoPizza: Pizza {}

The default can be overriden to handle the exceptional case:

class CornmealPizza: Pizza {
    override var crustGain: Grain { return .Corn }
}

Oops! This code is wrong and thankfully, the compiler spotted the error. Can you? The ‘r’ was missing in crustGain. Swift prevents such errors from slipping through the cracks by forcing the code to be explicit about overriding methods in classes. In this case, the code claimed an override, but the misspelled crustGain did not override anything. Here’s the fix:

class CornmealPizza: Pizza {
    override var crustGrain: Grain { return .Corn }
}

Now it compiles and works:

 NewYorkPizza().crustGrain 		// returns Wheat
 ChicagoPizza().crustGrain 		// returns Wheat
CornmealPizza().crustGrain 		// returns Corn

In addition to factoring out the common code, the Pizza superclass allows code to operate on pizzas without knowing the particular type of pizza, because a variable can be declared to refer a general pizza:

var pie: Pizza

But that general pizza reference can still be used to get specific information:

pie =  NewYorkPizza();		pie.crustGrain	 // returns Wheat
pie =  ChicagoPizza();  	pie.crustGrain	 // returns Wheat
pie = CornmealPizza();  	pie.crustGrain	 // returns Corn

Swift’s reference types (i.e. classes) have served this example well. But if this program involved concurrency, race conditions could be avoided by using Swift’s value types with their linguistic support for immutability. Let’s try pizza with value types.

Simple pizza values

Representing the three types of pizza with value types is as easy as it was for reference types, struct simply replaces class:

enum Grain { case Wheat, Corn }
struct  NewYorkPizza 	{ let crustGrain: Grain = .Wheat }
struct  ChicagoPizza 	{ let crustGrain: Grain = .Wheat }
struct CornmealPizza 	{ let crustGrain: Grain = .Corn  }

And it works:

 NewYorkPizza()	.crustGrain 	// returns Wheat
 ChicagoPizza()	.crustGrain 	// returns Wheat
CornmealPizza()	.crustGrain 	// returns Corn

Generalizing various pizzas with a protocol, and an undetected error

Using reference types, we were able to introduce a more general “pizza” concept with the addition of a single common superclass. The same generalization with value types will require two new items instead of one: a protocol to declare the common type:

protocol Pizza {}

and a protocol extension to define the attributes of the new type:

extension Pizza {  var crustGrain: Grain { return .Wheat }  }

struct  NewYorkPizza: Pizza { }
struct  ChicagoPizza: Pizza { }
struct CornmealPizza: Pizza {  let crustGain: Grain = .Corn }

This code compiles, and can be tested:

 NewYorkPizza().crustGrain 		// returns Wheat
 ChicagoPizza().crustGrain 		// returns Wheat
CornmealPizza().crustGrain 		// returns Wheat  What?!

Something is wrong: cornmeal pizza is not made from wheat! Oops, I forgot the ‘r’ in crustGain again. But with value types, there’s no override keyword to help the compiler find my mistakes. This omission seems out of place in a language otherwise designed to include enough redundancy to help find one’s errors. Without help from the compiler, we’ll have to be more careful. As a general rule,

Double-check attribute names that override protocol extensions.

Ok, let’s fix the typo:

struct CornmealPizza: Pizza {  let crustGrain: Grain = .Corn }

And try it out:

 NewYorkPizza().crustGrain 		// returns Wheat
 ChicagoPizza().crustGrain 		// returns Wheat
CornmealPizza().crustGrain 		// returns Corn  Hooray!

Pizza in a variable, and a wrong answer

In order to discuss pizza without worrying about New York, Chicago, or cornmeal, the Pizza protocol can be used as the type of a variable:

var pie: Pizza

And that variable can be used to answer questions about different pizzas:

pie =  NewYorkPizza(); pie.crustGrain  // returns Wheat
pie =  ChicagoPizza(); pie.crustGrain  // returns Wheat
pie = CornmealPizza(); pie.crustGrain  // returns Wheat    Not again?!

Why did the program lie and say that the cornmeal pizza contained wheat? The code that Swift compiled for the crustGrain query ignored the actual value of the variable. The only information that the compiler allowed the compiled code to use was what could be known when the program was compiled, not what happened when the program ran. All that can be known at compile-time is that pie is a Pizza, and the Pizza protocol extension says Wheat, so the declaration of a cornmeal crust in the CornmealPizza structure had no effect whatsoever when asking pie for something. Although the compiler could have warned about the potential for error from this use of static- instead of dynamic-dispatch, it did not. I believe that here lurks a trap for the unwary, and I would call this a major snare.

In this case, Swift provides a fix. In addition to defining the crustGrain attribute in the extension:

protocol  Pizza {}
extension Pizza {  var crustGrain: Grain { return .Wheat }  }

The code can declare the attribute in the protocol:

protocol  Pizza {  var crustGrain: Grain { get }  }
extension Pizza {  var crustGrain: Grain { return Wheat }  }

Presenting Swift with both a declaration and a definition in this fashion causes the compiler to take notice of the runtime value of the pie variable. (But not always, Had we not defined crustGrain in the extension, the declaration of crustGrain in the protocol would mean that every aggregate [structure, class, or enumeration] that inherits from Pizza must implement crustGrain.)

An attribute declaration in a protocol has two different meanings, static- vs dynamic-dispatch, depending on whether or not the attribute is defined in an extension.

With the addition of the declaration, the code works:

pie =  NewYorkPizza();  pie.crustGrain	 // returns Wheat
pie =  ChicagoPizza();  pie.crustGrain	 // returns Wheat
pie = CornmealPizza();  pie.crustGrain	 // returns Corn    Whew!

This facet of Swift is a serious snare; even after it had become clear, it continued to infest my code with bugs. Thanks to Alexandros Salazar who has a nice writeup of this issue. There is no compile-time check for this mistake (as of this writing 12/23/15, Xcode 7.2). To avoid this pitfall:

For every attribute defined in a protocol extension, declare it in the protocol itself.

However, this circumvention is not always possible…

Imported protocols cannot be fully extended

Frameworks (and libraries) let a program import interfaces to code without including all of the implementation. For example Apple provides many frameworks that implement the user experience, system facilities, and other functions. Swift’s extension facility allows a program to add its own attributes to imported classes, structures, enumerations, and protocols. For the concrete types (classes, structures, and enumerations), an attribute added via an extension works just as well as if it had been present in the original definition. But an attribute defined in a protocol extension is not such a first-class citizen of the protocol because it is impossible to add a declaration with a protocol extension.

Let’s try to import a framework defining pizzas and extend it to handle crusts. The framework defines the protocol, and the concrete types:

// PizzaFramework:

public protocol Pizza { }

public struct  NewYorkPizza: Pizza  { public init() {} }
public struct  ChicagoPizza: Pizza  { public init() {} }
public struct CornmealPizza: Pizza  { public init() {} }

and we’ll import the framework and extends pizzas with crust information:

import PizzaFramework

public enum Grain { case Wheat, Corn }

extension Pizza         { var crustGrain: Grain { return .Wheat	} }
extension CornmealPizza { var crustGrain: Grain { return .Corn	} }

Just as before, static-dispatch yields a bad answer:

var pie: Pizza = CornmealPizza()
pie.crustGrain                            // returns Wheat   Wrong!

This is because (as explained previously) the crustGrain attribute is not declared in the protocol, but only defined in the extension. However, without editing the source code in the framework, we cannot fix this problem. Hence, it is impossible to safely extend a protocol declared in another framework (without gambling that it will never need dynamic-dispatch.) To avoid this problem:

Do not extend an imported protocol with a new attribute that may need dynamic dispatch.

As in any large system, the quantity of features in Swift create an exponential number of interactions with the potential for adverse consequences. As just described, frameworks interact with protocol extensions to limit the utility of the latter. But frameworks aren’t the only problem, type restrictions can also interact adversely protocol extensions.

Attributes in restricted protocol extensions: declaration is no longer enough

When extending a generic protocol with attributes that apply to only some of the possible types, one can define them in a restricted protocol extension. But the semantics may not be what is expected.

Recall our running pizza example:

enum Grain { case Wheat, Corn }

protocol  Pizza { var crustGrain: Grain { get }  }
extension Pizza { var crustGrain: Grain { return .Wheat }  }

struct  NewYorkPizza: Pizza  { }
struct  ChicagoPizza: Pizza  { }
struct CornmealPizza: Pizza  { let crustGrain: Grain = .Corn }

Let’s make a meal out of pizza. Sadly, not all meals do include pizza, so we’ll accomodate different types of main dishes with a type parameter of a generic Meal structure:

struct Meal<MainDishOfMeal>: MealProtocol {
    let mainDish: MainDishOfMeal
}

Meal inherits from MealProtocol protocol to allow the code to test if a meal is gluten-free. We use of a protocol in order to allow the gluten-free code to be shared with meals that have other representations, for examples a meal without a main dish.

protocol MealProtocol {
    typealias MainDish_OfMealProtocol
    var mainDish: MainDish_OfMealProtocol {get}
    var isGlutenFree: Bool {get}
}

To avoid poisoning people—better safe than sorry—the code includes a conservative default:

extension MealProtocol {
    var isGlutenFree: Bool  { return false }
}

Happily, there is one dish that is OK: pizza made with corn instead of wheat. Swift’s where construction provides a way to express this case as a restricted protocol extension. When the main dish is pizza, we know it has crust, so it is safe to ask it about the crust. Without the restrictive where clause, the code would be unsafe:

extension MealProtocol  where  MainDish_OfMealProtocol: Pizza {
    var isGlutenFree: Bool  { return mainDish.crustGrain == .Corn }
}

The extension with a where is called a restricted extension.

Let’s get cooking with a nice cornmeal pizza!

let meal: Meal<Pizza> = Meal(mainDish: CornmealPizza())

And double-check our dish:

meal.isGlutenFree	// returns false
// But there is no gluten! Why can’t I have that pizza?

As shown in a previous section, a declaration in a protocol was sufficient to induce dynamic dispatch for a defined of the corresponding attribute in the protocol extension. But a definition in a restricted extension is always statically-dispatched. To prevent bugs caused by unexpected static-dispatch:

Avoid extending a protocol with a restriction if the new attribute might need dynamic dispatch.

Even if one avoids the snares related to protocol extensions, there are other problematic constructions in Swift. Most of these are mentioned in Apple’s Swift book, but since they may be more salient when isolated, we include a discussion of them below.

Optional chaining for assignment and side-effects

Swift’s optional types can prevent errors by providing static checking for the possilibity of a nil value. It provides a convenient shorthand, optional chaining, to handle cases where a nil value can be ignored, as was the default in Objective-C. Unfortunately, a detail of Swift’s semantics for optional chaining when the potentially-nil reference is the target of an assignment can result in errors. Consider an object that holds an integer, a possibly-nil pointer to it, and an assignment:

class Holder  { var x = 0 }
var n = 1
var h: Holder? = ...
h?.x = n++
n  // 1 or 2?

The final value of n depends on whether h is nil or not! If h is not nil, the assignment happens, the increment operator happens, and n ends up holding 2. But if h is nil, not only the assignment is skipped, but the increment is also skipped, and n ends up holding 1. In order to avoid surprises caused by missing side-effects,

Avoid assigning the result of an expression with side-effects to left-hand-side with optional chaining.


Functional programming snares in Swift

Swift’s support for functional programming brings the ability to apply the benefits of that paradigm to the Apple ecosystem. Swift functions and closures are first-class entities, convenient to use, with powerful abilities. Unfortunately, there are a couple of traps to avoid here.

In-out parameters silently fail with closures

A Swift in-out parameter allows a function to receive a value from a variable in the caller and then set the value of that variable. A Swift closure supports references to functions captured in mid-execution. Each of these contribute to elegant and expressive code, so you may be tempted to use them together, but the combination can be problematic.

Let’s rewrite the crustGrain attribute to illustrate an in-out parameter. We’ll start simply, without a closure:

enum Grain { case Wheat, Corn }

struct CornmealPizza {
    func setCrustGrain(inout grain: Grain)  { grain = .Corn }
}

To use the function, we pass it a variable. After the function returns, the variable’s value has been changed from Wheat to Corn.

let pizza = CornmealPizza()
var grain: Grain = .Wheat
pizza.setCrustGrain(&grain)
grain		// returns Corn

Now, let’s try a function that returns a closure that sets an grain parameter:

struct CornmealPizza {
    func getCrustGrainSetter()   ->   (inout grain: Grain) -> Void {
        return { (inout grain: Grain) in grain = .Corn }
    }
}

Using this closure merely requires on more invocation:

var grain: Grain = .Wheat
let pizza = CornmealPizza()
let aClosure = pizza.getCrustGrainSetter()
grain			// returns Wheat (We have not run the closure yet)
aClosure(grain: &grain)
grain			// returns Corn

So far so good, but what happens if we pass in the grain parameter to the closure-creator instead of the closure itself?

struct CornmealPizza {
    func getCrustGrainSetter(inout grain: Grain)  ->  () -> Void {
        return { grain = .Corn }
    }
}

And trying it:

var grain: Grain = .Wheat
let pizza = CornmealPizza()
let aClosure = pizza.getCrustGrainSetter(&grain)
grain				// returns Wheat (We have not run the closure yet)
aClosure()
grain				// returns Wheat  What?!?

In-out parameters do not work when passed into the outer scope of a closure, so

Avoid in-out parameters in closures.

This problem is mentioned in the Swift book, but there is a related issue concerning the equivalence of currying to creating a closure.

In-out parameters expose an inconsistency with currying

For a function that creates and returns a closure, Swift provides a compact syntax for the function’s type and body. Although this currying syntax is supposed to be a mere shorthand, it harbors a surprise when used with in-out parameters. To reveal the surprise, let’s try the same example with the special currying syntax: instead of declaring the function type’s as returning a function, the code includes a second parameter list after the first, and it omits the explicit closure creation:

struct CornmealPizza {
    func getCrustGrainSetterWithCurry(inout grain: Grain)() -> Void {
        grain = .Corn
    }
}

Just as in the explicit closure creation form, invoking this function returns a closure:

var grain: Grain = .Wheat
let pizza = CornmealPizza()
let aClosure = pizza.getCrustGrainSetterWithCurry(&grain)

But where the explicitly-created closure above failed to set the in-out parameter, this one succeeds!

aClosure()
grain				// returns Corn

Currying works for in-out parameters where explicit closure-creation fails.

Avoid currying with in-out parameters because the code will fail if you later change it to explicitly create a closure.


Summary

Apple’s Swift language has been carefully crafted to optimize the creation of software. As with any ambitious undertaking, a handful rough edges remain that can result in a program not working as expected. In order to avoid such unpleasant surprises, let’s review these snares so you can avoid them in your own code:

Double-check attribute names that override protocol extensions.

For every attribute defined in a protocol extension, declare it in the protocol itself.

Do not extend an imported protocol with a new attribute that may need dynamic dispatch.

Avoid extending a protocol with a restriction if the new attribute might need dynamic dispatch.

Avoid assigning the result of an expression with side-effects to left-hand-side with optional chaining.

Avoid in-out parameters in closures.

Avoid currying with in-out parameters because the code will fail if you later change it to explicitly create a closure.

Author: David Ungar

I'm fascinated by programming languages and their connection to thought. I tweet at @senderPath.

Leave a Reply

Your email address will not be published. Required fields are marked *