Utilizing value semantics in Swift

One really interesting aspect of Swift’s overall design is how centered it is around the concept of value types. Not only are most of the standard library’s core types (like String, Array and Dictionary) modeled as values, even fundamental language concepts — such as optionals — are represented as values under the hood.

What makes value types unique are the semantics around how they’re passed between the various parts of a program, as well as how mutations are applied to a given instance. This week, let’s take a look at a few different ways in which we can make use of those semantics — and how doing so could significantly improve the flexibility of our value-based code.

Basics available: The basics of value and reference types, and the differences between them, are explained in this article.

Unlocking local mutations

In general, the less mutable state that a program contains, the fewer are the chances for errors to occur. When things are kept immutable, they’re inherently more predictable, since there’s no chance of unexpected changes happening. However, making something immutable also often means sacrificing flexibility, which can sometimes become problematic.

Let’s say that we’re working on an app that deals with videos, and in order to make our core Video model as predictable as possible, we’ve chosen to make all of its properties immutable by defining them using let:

struct Video {
    let id: UUID
    let url: URL
    let title: String
    let description: String
    let tags: Set<Tag>
}

The above may seem like a good idea at first, especially if the only current way to obtain Video instances are to decode them from data downloaded over the network. However, thanks to the power of value semantics, doing something like the above is most often not necessary.

Not only are structs immutable by default unless they’re stored in a variable that itself is mutable, any mutations made to an instance of a struct will also always just be applied to the local copy of that value. That means that even if we were to open our Video type up for mutations, we wouldn’t risk introducing bugs caused by unhandled state changes.

So let’s do just that, by using var to define most of our model’s properties, rather than let. We won’t, however, make that change to all properties — since we want some of them to always remain constant — such as id and url in this case:

struct Video {
    let id: UUID
    let url: URL
    var title: String
    var description: String
    var tags: Set<Tag>
}

By performing the above change we both make it crystal clear what parts of a Video model that could potentially change in the future (even if those mutations happen elsewhere, such as on our server), but we also unlock new use cases for that model — such as using it to keep track of local state.

Let’s say that we’re adding a new feature to our video app, which lets our users perform local edits to a video that they’ve previously downloaded. Since we’ve now opened our Video model up for local mutations, we could simply let such a video editor operate directly on an instance of our model — like this:

class VideoEditingViewController: UIViewController {
    private var video: Video

    ...

    func titleTextFieldDidChange(_ textField: UITextField) {
        textField.text.map { video.title = $0 }
    }

    func tagSelectionView(_ view: TagSelectionView,
                          didAddTagNamed tagName: String) {
        video.tags.insert(Tag(name: tagName))
    }
}

The beauty of value semantics in the above scenario is that any local mutations that VideoEditingViewController makes to its private Video value won’t be propagated elsewhere. That means that we’re free to work on that value in complete isolation, and all of the user’s edits can be kept clearly separated from the original data source.

On the other hand, whenever we do want to keep any Video instance completely immutable, all we have to do is to reference it using a let — and no mutations will be allowed:

struct SearchResult {
    let video: Video
    let matchedQuery: Query
}

When it comes to our core data models, like the above Video type, letting the enclosing context decide whether to allow mutations or not — rather than baking those decisions into each model — often makes our model code much more flexible, without introducing any substantial risks, all thanks to value semantics.

Ensuring data consistency

However, sometimes we do need to exercise a bit more control when it comes to how a model is allowed to be mutated, especially if different parts of that model are somehow connected or dependent on each other.

For example, let’s now say that we’re working on a shopping app which contains a ShoppingCart model. Besides storing an array of Product values that the user has added to their shopping cart, we also store the total price of all products as well as their IDs — to both avoid having to recalculate the total price each time it’s accessed, and to enable constant-time lookup of whether a given product has already been added:

struct ShoppingCart {
    var totalPrice: Int
    var productIDs: Set<UUID>
    var products: [Product]
}

The above setup gives us both great flexibility and great performance, since many of the common operations that we’ll perform on a ShoppingCart instance can be executed in constant time — however, in this case, making everything mutable has also added a significant risk for our data to become inconsistent.

Not only do we have to always remember to update the totalPrice and productIDs properties whenever we add or remove a product, each of those properties could also be mutated at any time — without any change in products. That’s not great, but thankfully there’s a solution that lets us keep using value semantics, but in a slightly more controlled fashion.

Instead of making every property fully mutable, let’s restrict most mutations to only be allowed within the ShoppingCart type itself, by using the private(set) access modifier. We’ll then perform those mutations only in direct response to a change in the products array, using a property observer — like this:

struct ShoppingCart {
    private(set) var totalPrice = 0
    private(set) var productIDs: Set<UUID> = []
    var products: [Product] {
        didSet { productsDidChange() }
    }

    init(products: [Product]) {
        self.products = products
        // Note how we need to manually call our handling
        // method within our initializer, since property
        // observers aren't triggered until after a value
        // has been fully initialized.
        productsDidChange()
    }

    private mutating func productsDidChange() {
        totalPrice = products.reduce(0) { price, product in
            price + product.price
        }

        productIDs = []
        products.forEach { productIDs.insert($0.id) }
    }
}

With the above change, we’re still making full use of value semantics for our products property, while now also being able to guarantee complete data consistency throughout our model.

Simplifying repeated mutations

While value semantics give us a ton of benefits in terms of limiting how and where mutations can occur, sometimes those limitations can make certain pieces of code a bit more complex than they need to be.

Here we’re working on a type that lets us model an image rendering pipeline as a series of closure-based operations that get applied to a RenderingContext struct. Each operation is passed the previous context as input, mutates it, and then returns the updated value as output. Finally, once all operations have been performed, we take the final context value and use it to generate an image:

struct RenderingPipeline {
    var operations: [(RenderingContext) -> RenderingContext]

    func render() -> Image {
        var context = RenderingContext()

        context = operations.reduce(context) { context, operation in
            operation(context)
        }

        return context.makeImage()
    }
}

For more information about reduce, check out “Transforming collections in Swift”.

The above works, but there’s a catch. Within each closure operation, we’ll both need to manually copy the current context into a mutable variable, and once we’ve performed our mutations we also need to explicitly return the updated value — like this:

extension RenderingPipeline {
    mutating func fill(with color: Color) {
        operations.append { context in
            var context = context
            context.changeFillColor(to: color)
            context.fill(rect: Rect(origin: .zero, size: context.size))
            return context
        }
    }
}

The above may not seem like a big deal, and if the number of operations that we can perform are kept to a minimum, it probably won’t be. However, always having to copy and return the current rendering context does require us to write a fair amount of boilerplate, so let’s see if we can do something about that.

While we want to let RenderingContext remain a struct, let’s actually pass it by reference rather than by value when calling each operation — using the inout keyword. That way we can simply keep mutating the same context value all throughout our pipeline:

struct RenderingPipeline {
    var operations: [(inout RenderingContext) -> Void]

    func render() -> Image {
        var context = RenderingContext()
        operations.forEach { $0(&context) }
        return context.makeImage()
    }
}

Using the inout keyword doesn’t actually pass a pointer to our value, but rather gives us the same top-level behavior as when using a reference type, by automatically creating a mutable copy and assigning the resulting value back to the variable that was passed in.

Not only does the above change make our RenderingPipeline type much simpler, it also now lets us mutate each context directly within our operations — no copying or returning of values required:

extension RenderingPipeline {
    mutating func fill(with color: Color) {
        operations.append { context in
            context.changeFillColor(to: color)
            context.fill(rect: Rect(origin: .zero, size: context.size))
        }
    }
}

While the inout keyword should definitely be used with some amount of caution, since it does circumvent some of the safeguards that value types give us in terms of avoiding shared state — when used internally within a type, like we did above, it can again give us some very real benefits without much added risk.

Conclusion

Fully utilizing value types in terms of how they handle mutations, and how they ensure that state is kept local by default, can be a great way to improve both the stability and flexibility of our model code.

However, making everything mutable by default isn’t necessarily the best approach — sometimes we do need to lock things down a bit both to ensure data consistency, and to send a clear signal as to what data that’s never supposed to be modified.

Finally, using the inout keyword can let us keep leveraging the power of value types — while also introducing some of the convenience of reference types, when used in the right situations.

What do you think? How do you pick between using a value type and a reference type, and will any of the techniques from this article help you make your model code more flexible? Let me know, either via Twitter or email.

Thanks for reading! 🚀

Rule-based logic in Swift

Generalizing Swift code