Search

Protocol-Oriented Problems and the Immutable ‘self’ Error

Matt Mathias

7 min read

Apr 28, 2016

Protocol-Oriented Problems and the Immutable ‘self’ Error

Protocol-oriented programming is a design paradigm that has a special place in Swift, figuring prominently in Swift’s standard library. Moreover, protocol-oriented programming leverages Swift’s features in a powerful way.

Protocols define interfaces of functionality for conforming types to adopt, providing a way to share code across disparate types. By conforming to protocols, value types are able to gain features outside of their core definition despite their lack of inheritance. Furthermore, protocol extensions allow developers to define default functionality in these interfaces. And so, conforming types can gain an implementation simply by declaring their conformance. These features allow you to produce more readable code that minimizes interdependencies between your types, and also allows you to avoid repeating yourself.

That said, there are practical concerns. Protocols can sometimes seem to force a decision between whether you want both value types and reference types to be able to conform. Obviously, this decision has the potential to limit the application and usability of a given protocol. This post provides an example where the developer may want to make this limiting choice, elucidates the tensions in making the decision, and discusses some strategies in moving forward.

An Example

Here is a simple example that provides a Direction enum, a VehicleType protocol and a class Car that conforms to the protocol. There is also a protocol extension that provides a default implementation for a method required by the VehicleType protocol.

enum Direction {
    case None, Forward, Backward, Up, Down, Left, Right
}

protocol VehicleType {
    var speed: Int { get set }
    var direction: Direction { get }
    mutating func changeDirectionTo(direction: Direction)
    mutating func stop()
}

extension VehicleType {
    mutating func stop() {
        speed = 0
    }
}

class Car: VehicleType {
    var speed = 0
    private(set) var direction: Direction = .None

    func changeDirectionTo(direction: Direction) {
        self.direction = direction

        if direction == .None {
            stop()
        }
    }
}

If you have been typing along with this example, you should see the following error within the changeDirectionTo(_:) method’s implementation:

Immutable Self Error

What does “Cannot use mutating member on immutable value: ‘self’ is immutable” mean?

Understanding the Error

There are three insights that help to clarify the error.

  1. Recall that the protocol VehicleType declares that stop() is a mutating method. We declared it like this because stop() changes the value of a property, speed, and we are required to mark it as mutating in case value types conform.
  2. VehicleType’s protocol extension provides a default implementation for stop().
  3. Car is a reference type.

mutating

mutating methods signal to the compiler that calling a method on a value type instance will change it. The mutating keyword implicitly makes the instance itself—self—an inout parameter, and passes it as the first argument into the method’s parameter list. In other words, the compiler expects stop() to mututate self.

VehicleType’s Protocol Extension

The protocol extension provides a default implementation for stop() that simply sets the vehicle’s speed property to 0. Since it modifies a property, it needs to be declared as mutating. This declaration of mutating is significant: all conforming types will have an implementation of stop() that is mutating.

Car is a Reference Type

Notice that we call stop() within Car’s implementation of changeDirectionTo(_:). If the new direction is .None, then we infer that the Car instance needs to stop. But here is where the problem occurs.

Putting it all Together

stop() is a mutating method, with a default implementation, that the compiler expects will change self. But Car is a reference type. That means the self—the reference to the Car instance that is used to call stop()—that is available within changeDirectionTo(_:) is immutable!

Thus, the compiler gives us an error because stop() wants to mutate self, but the self available within changeDirectionTo(_:) is not mutable.

Three Solutions

There are three principal ways to solve this problem.

  1. Implement a non-mutating version of stop() on Car.
  2. Mark the VehicleType protocol as class only: protocol VehicleType: class { ... }.
  3. Make Car a struct.

A Non-mutating Version of stop()

One solution is to implement a non-mutating version of stop() on Car. At first glance, this may appear to be the most flexible solution. It preserves the ability of value types to conform to VehicleType, and maintains its relevance to reference types as well.

It is important to understand that a mutating method and a non-mutating method are not the same. mutating methods have a different parameter list than a non-mutating method. A mutating method’s first parameter is expected to be inout, with the remainder of the parameter list being same. A non-mutating method does not include this first inout parameter. Thus, you are not quite repeating yourself in strict sense—they are indeed different methods.

Two Different Stop Methods

The first signature for stop() in the image refers to the implementation that we just added to Car. The second refers to the default implementation that was added in the protocol extension. Thus, our implementation of stop() on Car adds a whole new method that provides clarity and context.

If we choose this solution, our Car class now looks like so:

class Car: VehicleType {
    var speed: Int = 0
    private(set) var direction: Direction = .None

    func stop() {
        speed = 0
    }

    func changeDirectionTo(direction: Direction) {
        self.direction = direction

        if direction == .None {
            stop()
        }
    }
}

Choosing Class

The next two solutions make for a different decision: should the VehicleType protocol apply to classes, value types or both (as above)?

Choosing a class-only protocol will solve the problem insofar that we will have to remove mutating keywords from the protocol’s declaration. Conforming types will have to be classes, and will thereby not have to worry about the added confusion resulting from having a default implementation of a mutating method. In this way, the protocol becomes a bit more clear in its specificity.

Choosing Value Types

Choosing a value-type-only protocol is not technically possible. There is no syntax available for limiting a protocol in this way (e.g., protocol VehicleType: value { ... } syntax). If you want to pursue this route, you would leave the protocol as it currently is, and change Car to be a struct. Perhaps it would be useful to add some documentation to VehicleType so that users can see that it is intended for value types only:

/// `VehicleType` is a protocol that is intended only for value types.
protocol VehicleType {
    // protocol requirements here
}

This option is appealing if you have no good reason to make Car a class. In Swift, we often start writing our models as a value type before reaching for the complexity of a reference type. More still, if you follow the advice in the video linked to above, you may even want to start your modeling by defining a protocol. Either way, reaching for a class should not be your first choice unless you absolutely know that you need a class.

What Should You Do?

The more flexible solution presented above suggests that perhaps there is good reason for Car to be a class. If that is the case, then stop() will have special meaning there (i.e., it will need to not be mutating). More specifically,stop() will be a different method on a class that it will be on a value type.

And perhaps there are also good reasons for value types to conform to VehicleType But if this is true, then there are some issues to think about. There are three disadvantages to taking the more ‘flexible’ path.

  1. Allowing both value types and reference types to conform to VehicleType makes the code a bit more complex. Readers of our code will have to take some time and think about the differences between implementations of stop() on classes vs implementations of stop() on value types.

  2. Choosing the flexible option means that you will likely have to type more code. You will have to provide non-mutating implementations of each mutating method required by the protocol that has a default implementation. This code will feel like you are repeating yourself, which is not great.

  3. Finally, and perhaps more importantly, you should be asking yourself if your architecture is getting confusing. Is this protocol providing an interface that you want your model types to conform to? In the example here, VehicleType suggests that this protocol provides an interface to model types. If you find yourself in a similar situation, it probably doesn’t make sense for your model types to variously be either classes or value types if your models need the functionality listed in the VehicleType protocol.

For example, your models will probably need to take advantage of Core Data, KVO and/or archiving. If any of these are the case, then you’ll want to restrict conformance to your protocol to classes. If any of these are not actually the case, then perhaps you will want to restrict conformance (at least, informally by way of inline documentation) to value types.

A very informal glance at the Swift standard library suggests that every protocol that includes the mutating keyword is intended to be conformed to by a value type (e.g., CollectionType). One simple rule of thumb here may be that if you use the keyword mutating in a protocol, then you are intending for only value types to conform to it.

Josh Justice

Reviewer Big Nerd Ranch

Josh Justice has worked as a developer since 2004 across backend, frontend, and native mobile platforms. Josh values creating maintainable systems via testing, refactoring, and evolutionary design, and mentoring others to do the same. He currently serves as the Web Platform Lead at Big Nerd Ranch.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News