Controlling Complexity in Swift — or — Making Friends with Value Types

Video & transcription below provided by Realm: a replacement for SQLite & Core Data with first-class support for Swift!

People tell you that you’re supposed to avoid mutable state, but how can anything happen if you never call a setter? People tell you that you’re supposed to write unit tests, but how can you test a user interaction? People tell you that you’re supposed to make your code reusable, but how can you factor it out of your enormous view controllers?

Andy Matuschak is an iOS Developer at Khan Academy, and previously helped build iOS 4.1–8 on the UIKit team. In this talk, he presents a pragmatic approach to managing complexity in inherently stateful systems.


Overview of Complexity (00:00)

Complexity is a really broad, but important term because we’re making software which doesn’t just exist at one time. It exists, say on one day, then requirements change a week later, or it exists for one release cycle and then your customers change. Change and complexity are enemies because complexity makes everything harder to change.

Taste vs. Ability (1:13)

The issue comes up sometimes with the gulf that we can have between our taste and our ability. Often, we find ourselves looking at code we’ve written. We think that it’s ugly, brittle, and likely to break in the future, but we don’t yet have the ability to fix it. Let’s explore that tension and see if we can do a little bit about it, particularly with respect to complexity today and in Swift.

Focusing On… (1:50)

  • The paths through our system: When we try to explain something to a co-worker, we often walk over to a whiteboard and start drawing a tree diagram. This diagram represents a path of some kind in our application, and so we’ll talk about techniques to think more carefully about the paths in our application.
  • Control flow: When we look at some method, some block of code, we have to think about how many ways that code could execute. Is it going to be triggered by the owner of this object, or by some notification or timer? What can we say about the paths into a node?
  • Dependency pathways: When we look at code, or an interface, we also want to think about how many other components it might be communicating with. If we were to draw the dependency graph of the applications architecture, would it be a hairy mess or a nice tree?
  • Effects: This is like an operation tree within your application. When we look at some method, can we tell what effects it could have on the rest of the system, and is that set of effects reliable?

By thinking about these various paths in an application and focusing them, we’ll be able to focus our thought, and in turn our understanding as well.

Values (03:57)

There’s one really concrete technique that can help you remove complexity in your application. Values are even easier to approch in Swift. They’re really familiar things: numbers, strings, and so on.

1. The Value of Values (04:30)

Why has Swift added extra value to values? Objective-C is an aggressively object-oriented language. The idiomatic, typical way of approaching a new module of code is to make a new interfact. But if we look at Swift’s standard library, we see this:

$ grep -e "^struct " stdlib.swift | wc -l
      78
$ grep -e "^enum " stdlib.swift | wc -l
       8

Almost everything in the Swift standard library is what we would call a value type, which have become vastly more powerful. They are all types which are copied on assignment, and have a single owner. Numbers and booleans are all value types in Objective-C as well. Things which are not, however, are strings, arrays, and dictionaries. In the following code, we have an array with the values 1, 2, and 3. We make a new variable B and append 4 to it. However, A remains the same. This is new behaviour in Swift, and structs and enums also behave this way.

var a = [1, 2, 3]
var b = a

// a = [1, 2, 3]; b = [1, 2, 3]
b.append(4)
// a = [1, 2, 3]; b = [1, 2, 3, 4]

Everything else that you’re dealing with are reference types, which means that on assignment, they’re shared. They have multiple owners. That includes objects. When you make a view, if you assign that view to another variable and change the alpha around, both of those variables are still referring to the same view. They’re sharing a reference. Closures are also reference types.

var a = UIView()
var b = a

// a.alpha = 1; b.alpha = 1
b.alpha = 0
// a.alpha = 0; b.alpha = 0

Intuition (07:06)

Objects are really just like objects in the real world. When we have a dog, Fido, in the physical world, he runs around and is persistent over time. If I go out to brunch with you and tell you about this crazy thing that Fido did, you imagine the same dog in your head that I am talking about. The reason for that is, I’m really transmitting to you his name, or a label that you are responsible for hooking up to some underlying representation. Objcets are really like passing names of things around the system, which you’re familiar with if you understand pointers.

By contrast, values are like data. If I instead send you a spreadsheet and you were to calculate a total or average, your number would not be affected if I went home and changed my spreadsheet. The spreadsheet is just a spreadsheet, and these numbers are just numbers. Values are simpler, which may seem bad if you think of that as “less powerful”, but sometimes you do want the less powerful thing. The simplicity of values is great exactly because you don’t have to worry about a bunch of things when you use them.

In Swift, it’s vastly easier to express your ideas in terms of values. We can do things like defining methods on structs, or this much more complex case, we can make value types which contain collections. We can make nested types like the Tool type, which is a child of the DrawingAction type. The branches in the enums can contain values, so the Tool is either a Pencil with a color or an Eraser with a width. The important thing to remember is that Swift is made for this, which can be seen both in the semantics it expresses as well as in its standard library.

struct Drawing {
  var actions: [DrawingAction]
}

struct DrawingAction {
  var samples: [TouchSample]
  var tool: Tool

  enum Tool {
    case Pencil(color: UIColor)
    case Eraser(width: CGFloat)
  }
}
struct TouchSample {
  var location: CGPoint
  var timestamp: NSTimeInterval
}

Why Values are Important: The Three I’s (09:53)

  • Inert (10:09)
    It’s pretty hard to make a value type that behaves, especially over time. It’s typically inert, a hunk of data like the spreadsheet, storing data and exposing methods that perform computations on that data. What that means is that the control flow is strictly controlled by the one owner of that value type. That makes it vastly easier to reason about code that will only be invoked by one caller. This, in contrast, is set up with a view, which is a very typical thing.

    This particular view has some number of bloops, and when we change the number of bloops, we kick off an animation that changes the center position of this view. Upon completion of the animation, the center changes back to zero. But questions come up, like what is the semantic if somebody else changes center.x to some other value while the animation’s running, or what happens if the method is called multiple times back to back? The correct semantic is not clear in this implementation. With value types like structs, you simply can’t write this kind of code because doing so would require giving the animation a timer system, a reference to the struct instance, which you can’t do. The inertness of the values really comes directly out of their single ownership.

class MyView {
  var numberOfBloops: Int {
  didSet {
    UIView.animateWithDuration(1, animations: {
      center.x = numberOfBloops * 10
    }, completion: { _ in
        center.x = 0
    })
  }
  }
}
  • Isolated (12:09)
    Reference types create implicit dependencies, or dependencies in the application structure that can’t be seen. In this example, there’s some model type User that is a reference type. Some view controller has a user and might change over time. Then there’s some networking operation that you make with the user. All these are referring to the same user - when the networking operation is created with a user from the view controller, that’s Fido. The networking operation now has an implicit dependency on the behaviour of the view controller. This means that if one of your teammates adds some extra method to the view controller, that affects the behaviour of the networking operation but is not clear at all from the code.
class User { ... }

class ViewController {
  var user: User
}

class NetworkOperation {
  init(user: User)
}
  • Interchangeable (13:46)
    Finally, value types are interchangeable. This goes back to the point about not being able to replace Fido without Fido’s other owners being upset. Every time you assign a value to a new variable, that value is copied, and so all of those copies are completely interchangeable. If they have the same data in chem, you cannot tell the difference between them. This means that you can safely store a value that’s been passed to you. Interchangeability means that it doesn’t matter how a value was constructed, as long as it compares equal via equals equals.

    This is relevant because you can’t make a UITouch. If you pass it to something that’s expecting a UITouch argument, the same things will not necessarily happen, even though as far as you can tell, it contains the same data. This really limits what you can do with UITouches. You can’t write them down and save them, buffer them up, or deliver them over the network. Say you’re making a multi-user drawing app - you can’t do a lot of things, like smooth over time, because you can’t treat them interchangeably. Together, this means that you can’t move your boundaries.

struct Drawing {
  var actions: [DrawingAction]
}

struct DrawingAction {
  var samples: [TouchSample]
  var tool: Tool
  enum Tool {
    case Pencil(color: UIColor)
    case Eraser(width: CGFloat)
  }
}

struct TouchSample {
  var location: CGPoint
  var timestamp: NSTimeInterval
}

Regarding unit tests, unit testing UI is traditionally a really difficult thing. In part, that’s because the APIs in question are not interchangeable. When you’re doing tests with simple data in and data out, you don’t need mocking or stubbing. You can pass a value into a function, then look at the resulting value.

So, those are the three I’s of why value types are useful: they’re inert, they’re isolated, and they’re interchangeable. However, not all UI types behave this way, and so we can think of this as a little bit of a guide. If you see a value type, you can usually assume that it has these properties, but you usually can’t assume that about objects.

2. The Object of Objects (21:25)

However, I’m not trying to suggest that you replace your entire program with values. There is still some utility to objects within your system. Objects are the things in your system which behave and respond, which is important to remember because values are dead. We need some way to reconcile this distinction between them, some way to create the notion of a stable entity within our system. The metaphor I like to use is a zoetrope, which is a classic animation device. The pictures around the periphery are not changing or mutating, but when we focus on a point in the mirror, we get the effect of a stable entity changing over time. Many dead values can make, together, one living identity.

In this model, the values are inert, but a sequence of those values can come together to create something which we can refer to as a persistent identity within our system. Going back to the drawing example, we can have the notion of a canvas, which evolves over time as it gets painted on. It has a current drawing that is inert, and any given drawing is a snapshot at one instance in time. If an identity is a series of values that are causally related and values are just these dead, inert snapshots, then state can be thought of as an identity’s value at a single time. This could be the current drawing, or the last drawing. Cribbed from Rich Hickey, we can think of values as inert data, which we can connect causally over time to create some notion of a stable identity. Finally, we can assign labels to places in that stream of time to have some useful semblance of state.

class CanvasController {
  var currentDrawing: Drawing
  /* ... */
}

struct Drawing { /* ... */ }

So, how do objects fit into the picture? Objects have identity by default in Swift. If you pass several around, they all refer to the same thing. We can use objects to create a stable picture of any definition of state that we would like in our application. They can manage state transitions and perform side effects, as these persistent elements which can behave. They’re not inert, and their utility is in their non-inertness. Here’s an example of how we might continue to flesh out this canvas. We have a canvas controller that could be a view controller, and a struct functioning as a drawing value. When we update the drawing in place, regardless of whether we call a mutating method on the drawing or whether we reassign a new drawing to it, the same inertness and deadness of that value is true. Those three properties of inertness, isolation, and interchangeability remain in that drawing, even though we have this outer canvas controller that is receiving events from multiple places and is modifying stuff over time.

class CanvasController {
  var currentDrawing: Drawing
  func handleTouch(touch: UITouch) {
    currentDrawing = ...
  }
}

3. Everything in Its Place (27:19)

After going over the three I’s, as well as how a sequence of values can create an identity, how do we get these things working together? My proposal is that you think of your program as being in two layers: the object layer and the value layer. The object layer is full of objects that are just a thin veneer that consume events, consult the value layer to compute a new value, then store it. All of the business logic of your application lives in the value layer. If we think about it in this way, we can really get to a picture where the object layer is tiny and the value layer contains the bulk of the application.

Why separate? One of the core tenets of object-oriented programming is that you separate logic from action. What I’m proposing here is just a much more aggressive way of accomplishing this. To return to the drawing example, we need to do several things to implement a drawing program:
- Incorporate touch into Drawing
- Estimate touch velocity, using some fancy filter
- Smooth touch sample curves, since we only get them at 60 hertz
- Compute stroke geometry, which will depend on the velocity, timing, and various other things
- Render the drawing

The traditional thing to do would be to make a UIView, but I propose that we can do better. If we start by looking at estimating touch velocity, that seems like something that can live in the value layer as a function which takes a bunch of touch samples and returns a point representing the velocity. We can use Bezier curves and paths to smooth touch sample curves and compute stroke geometry. This too can be readily tested and fully isolated from the rest of the system.

// estimate touch velocity
extension TouchSample {
  static func estimateVelocities(samples: [TouchSample])
    -> [CGPoint]
}

// smooth touch sample curves
extension TouchSample {
  static func smoothTouchSamples(samples: [TouchSample]) -> [TouchSample]
}

// compute stroke geometry
struct PencilBrush {
  func pathForDrawingAction(action: DrawingAction)
    -> UIBezierPath
}

// incorporate touch into drawing (+ update state)
extension Drawing {
  mutating func appendTouchSample(sample: TouchSample)
}

What we’re talking about doing here is creating a functional core for our application. We can write code functionally and make an imperative shell. I argue that we can do this as thinly as possible and make the core as thick as possible. I hope that this way of presenting the concept is a little more familiar to somebody who’s coming from industry and object-oriented development and who might be turned off by all the discussions in the functional programming domain.

Further Reading (34:57)

Q&A (35:55)

Q: Are you making any assumptions about mutability or immutability when you talk about values?
Andy: I’m not making any assumptions because of the way that mutability of value types works in Swift. When you mutate a value type in Swift, you are not mutating the value, but the variable which is holding the value. You cannot mutate a value.

Q: Could you say a bit more on the value of value types? Like structs and enums, as opposed to immutable values as a more general concept?
Andy: It is true that if you make a reference-type an object, you can still pass a reference to that thing to other parts of the system. The thing that makes me a little nervous about using objects with only immutable fields in place of values is that it is much easier to incrementally move them into not being inert, isolated, and interchangeable than it is for structs. In C, in C++, and even Objective-C to some extent, structs and enums have some additional semantics that really have to do with performance. In C, when you make a struct, it is generally on the stack, and is passed by copy. If you make an immutable reference in Objective-C or C, you’re going to have to do a heap allocation and do indirection, so there’s some performance ramifications. Those ramifications don’t exist in Swift, so a struct doesn’t necessarily always exist on a stack in Swift, and an object doesn’t necessarily always exist on the heap, so those distinctions kind of go away.

Q: You spoke about the upsides of moving more to values but are there any drawbacks?
Andy: As it stands in Swift right now, a lot of those optimizations don’t exist. If you make a bunch of structs and you start passing them all over the place, and you make new structs and you copy old structs into new structs, and so on, and so forth, you will get really poor performance characteristics. However, Swift has been carefully designed from a number of respects, such that, those optimizations can be made. Swift knows enough to pass stack-allocated values around by reference of the stack and can avoid a lot of the copies that you’re probably seeing right now if you do value-oriented programming in Swift. The special semantics of array make it harder to avoid, since it’s a value-type collection. If you make a new variable and assign an array into it and mutate it, you’ll cause a copy to happen, and the backing stores of the array will be shared. There are definitely still places where doing things more directly can lead to better performance characteristics.

Q: If you have your data model as values, it seems like you sort of go to a GL-like view of re-doing the user interface and redrawing at every step. Do you have any strategies for making that more efficient?
Andy: There’s a web framework called React that is roughly a value-oriented UI framework. The idea is that you have these components which you ask to render based on some data and they what seems to be a DOM tree but is in fact a representation of what the DOM tree would be. The React library is able to take that set of instructions that the component is returning and is able to diff them with respect to the tree of nodes that it already knows about. Modifying the real physical web page DOM would be super expensive, just like for us, making and destroying a view is super expensive.

Q: Sometimes it’s difficult to determine whether or not, for example, a function that would mutate a value type should either be a function that would return a new type with a mutation or simply be a mutating function. Do you have any guidelines to determine which to pick when?
Andy: The critical distinction between a mutating function and a function which creates a new value internally, mutates it and returns it, is really a degree of interchangeability, and a degree to which you can move boundaries around. If what you have is a constant value, you can’t use this thing that you’ve made without copying it into a variable, so in that respect, a mutating function is less composable.

Q: If we had a framework where we just clone values where we don’t want them to be modified, would that give more flexibility to the designer and more efficiency to make a call on when they want the values to be immutable, or where they don’t really care whether or not the value changes?
Andy: That’s the Objective-C strategy of making stuff conform to NSCopying. I would argue that it is better to make your data structures be values and put them in a box, where box is Class Box with var value. When you want to share a thing between multiple owners, you can use your Box; then you can know exactly when you have safety and when you don’t.

Q: What are your thoughts on coding in a mixed environment and moving code to Swift? From your perspective, how does the interoperability work?
Andy: I was overly optimistic about how incrementally I could do it. You make most things and you can’t use it, which is a reasonable constraint. However, it does make adopting the strategy I described, in an existing project, much more difficult. I will warn, from experience, those of you with large codebases and are thinking about adopting this approach, make sure that everyone is on board with this approach because there’s a significant cost to it.

Q: How do dependencies work out? It seems like the greatest value of using values is in the model layer, yet that’s the layer at which you have the most dependencies across the rest of your app, which is probably in Objective-C.
Andy: In my experience, we had a CoreData stack, which is the opposite of isolation. Our strategy was putting a layer about the CoreData layer that would perform queries and return values. But where would we add functionality in the model layer? As far as using values in the view layer, we do a lot of that actually. We have a table view cell all the way down the stack that will render some icon and a label. The traditional thing to do would be to pass the ManagedObject for that content to the cell, but it doesn’t need that. There’s no reason to create this dependency between the cell and everything the model knows about, and so we make these lightweight little value types that the view needs. The owner of the view can populate that value type and give it to the view. We make these things called presenters that given some model can compute the view data. Then the thing which owns the presenter can pass the results into the view.

Q: How do you feel about MVVM?
Andy: MVVM is not my answer to things, since my complaint with that is that, in MVVM, the view models both perform transformation between the model layer and the presentation data, which could be inert. Also, the view models perform effects. They might kick off a network request or something. I do not approve of entangling those two things.

Q: To what extent can you leverage the compiler to enforce, and to what extent does it interfere with your ability to guarantee that these things work the way you want them to across different skill levels/attitudes/mistakes from a team?
Andy: If you establish the principle that stuff lives in the value layer, it is harder for your colleagues to make mistakes. As far as the type system and things like that, those also make it much easier to infer certain requirements. Culturally, it’s also really challenging. A lot of the constructs that we might use to keep things extra safe are harder, in the sense that they’re not near to hand so they’re intimidating. In another talk, Functioning as a Functionalist, I go into more about how to deal with that culturally.

Q: Are implications or tips about your talk and how to use generics? For example, say you have some function that expects a generic type. Could that be an object or value type? What protocols would you reommend we think about?
Andy: The nice thing about generics is that you don’t have to think too hard about what the thing is. In Objective-C it was really hard to write general stuff, so I think the biggest, immediate win of generics is that you can write array. I’m hesitant to recommend requiring that a generic type be a value type. Some examples of interesting generic things we’ve been writing include an abstract data source for collection using table views. Everybody is also doing JSON decoding stuff in Swift.


See full transcription

Andy Matuschak

Andy Matuschak

Andy is the lead mobile developer at Khan Academy, where he’s creating an educational content platform that’s interactive, personalized, and discovery-based. Andy has been making things with Cocoa for 12 years—most recently, iOS 4.1–8 on the UIKit team at Apple. More broadly, he’s interested in software architecture, computer graphics, epistemology, and interactive design. Andy and his boundless idealism live in San Francisco, primarily powered by pastries.