Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
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.
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:
What does “Cannot use mutating member on immutable value: ‘self’ is immutable” mean?
There are three insights that help to clarify the error.
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.VehicleType
’s protocol extension provides a default implementation for stop()
.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 ExtensionThe 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 TypeNotice 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.
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.
There are three principal ways to solve this problem.
mutating
version of stop()
on Car
.VehicleType
protocol as class only: protocol VehicleType: class { ... }
.Car
a struct.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.
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()
}
}
}
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 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.
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.
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.
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.
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.
Our introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
The Combine framework in Swift is a powerful declarative API for the asynchronous processing of values over time. It takes full advantage of Swift...
SwiftUI has changed a great many things about how developers create applications for iOS, and not just in the way we lay out our...