Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
Objective-C had protocols. They name a set of messages. For example, the UITableViewDataSource
protocol has messages for asking the number of sections and the number of rows in a section.
Swift has protocols. They too name a set of messages.
But Swift protocols can also have associated types. Those types play a role in the protocol. They are placeholders for types. When you implement a protocol, you get to fill in those placeholders.
Associated types are a powerful tool. They make protocols easier to implement.
For example, Swift’s Equatable
protocol has a function to ask if a value is equal to another value:
static func ==(lhs: Self, rhs: Self) -> Bool
This function uses the Self
type. The Self
type is an associated type. It is always filled in with the name of the type that implements a protocol. (Not convinced Self
is an associated type? Jump to the end of the article, then come back.) So if you have a type struct Name { let value: String }
, and you add an extension Name: Equatable {}
, then Equatable.Self
in that case is Name
, and you will write a function:
static func ==(lhs: Name, rhs: Name) -> Bool
Self
is written as Name
here, because you are implementing Equatable
for the type Name
.
Equatable
uses the associated Self
type to limit the ==
function to only values of the same type.
NSObjectProtocol
also has a method isEqual(_:)
. But because it is an Objective-C protocol, it cannot use a Self
type. Instead, its equality test is declared as:
func isEqual(_ object: Any?) -> Bool
Because an Objective-C protocol cannot restrict the argument to an associated type, every implementation of the protocol suffers. It is common to begin an implementation by checking that the argument is the same type as the receiver:
func isEqual(_ object: Any?) -> Bool {
guard let other = object as? Name
else { return false }
// Now you can actually check equality.
Every implementation of isEqual(_:)
has to check this. It must check each and every time it is called.
Implementers of Equatable
never have to check this. It is guaranteed once and for all, for every implementation, through the Self
associated type.
Protocol ‘SomeProtocol’ can only be used as a generic constraint because it has Self or associated type requirements.
Associated types are a powerful tool. That power comes at a cost:
error: protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements
Code that uses a protocol that relies on associated types pays the price. Such code must be written using generic types.
Generic types are also placeholders. When you call a function that uses generic types, you get to fill in those placeholders.
When you look at generic types versus associated types, the relationship between caller and implementer flips:
Consider a function checkEquals(left:right:)
. This does nothing but defer to Equatable’s ==
:
func checkEquals(
left: Equatable,
right: Equatable
) -> Bool {
return left == right
}
The Swift compiler rejects this:
error: repl.swift:2:7: error: protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements
left: Equatable,
^
error: repl.swift:3:8: error: protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements
right: Equatable
^
What if Swift allowed this? Let us do an experiment.
Pretend you have two different Equatable types, Name
and Age
.
Then you could write code like this:
let name = Name(value: "")
let age = Age(value: 0)
let isEquals = checkEquals(name, age)
This is nonsense! There are two ways to see this:
==
would checkEquals
call in the last line? Name
’s? Age
’s? Neither applies. These are only ==(Name, Name)
and ==(Age, Age)
, because Equatable declares only ==(Self, Self)
. To call either Name’s or Age’s ==
would break type safety.Equatable
type is not a type alone. It has a relationship to another type, Self
. If you write checkEquals(left: Equatable, right: Equatable)
, you only talk about Equatable
. Its associated Self
type is ignored. You cannot talk about “Equatable
” alone. You must talk about “Equatable
where Self
is (some type)”.This is subtle but important. checkEquals
looks like it will work. It wants to compare an Equatable
with an Equatable
. But Equatable
is an incomplete type. It is “equatable for some type”.
checkEquals(left: Equatable, right: Equatable)
says that left
is “equatable for some type” and right
is “equatable for some type”. Nothing stops left
from being “equatable for some type” and right
from being “equatable for some other type”. Nothing makes left
and right
both be “equatable for the same type”.
Equatable.==
needs its left
and right
to be the same type. This makes checkEquals
not work.
Discover why a code audit is essential to your application’s success!
checkEquals
cannot know what “some type” should be in “Equatable
where Self
is (some type)”. Instead, it must handle every group of “Equatable
and Self
type”: It must be “checkEquals for all types T
, where T
is ‘Equatable
and its associated types’”.
You write this in code like so:
func checkEquals<T: Equatable>(
left: T,
right: T
) -> Bool {
return left == right
}
Now, every type T
that is an Equatable
type – this includes its associated Self
type – has its own checkEquals
function. Instead of having to write checkEquals(left: Name, right: Name)
and checkEquals(left: Age, right: Age)
, you use Swift’s generic types to write a “recipe” for making those types. You have walked backwards into the “Extract Generic Function” refactoring.
Writing checkEquals
using NSObjectProtocol
instead of Equatable
does not need generics:
import Foundation
func checkEquals(
left: NSObjectProtocol,
right: NSObjectProtocol
) -> Bool {
return left.isEqual(right)
}
This is simple to write. It also allows us to ask stupid questions:
let isEqual = checkEquals(name, age)
Is a name
even comparable with an age
? No. So isEqual
evaluates to false
. Name.isEqual(_:)
will see obj
is not a kind of Name
. Name.isEqual(_:)
will return false
then. But unlike Equatable.==
, every single implementation of isEqual(_:)
must be written to handle such silly questions.
Associated types make Swift’s protocols more powerful than Objective-C’s.
An Objective-C’s protocol captures the relationship between an object and its callers. The callers can send it messages in the protocol; the implementer promises to implement those messages.
A Swift protocol can also capture the relationship between one type and several associated types. The Equatable
protocol relates a type to itself through Self
. The SetAlgebra
protocol relates its implementer to an associated Element
type.
This power can simplify implementations of the protocol. To see this, you contrasted implementing Equatable’s ==
and NSObjectProtocol’s isEqual(_:)
.
This power can complicate code using the protocol. To see this, you contrasted calling Equatable’s ==
and NSObjectProtocol’s isEqual(_:)
.
Expressive power can complicate. When you write a protocol, you must trade the value of what you can say using associated types against the cost of dealing with them.
I hope this article helps you evaluate the protocols and APIs you create and consume. If you found this helpful, you should check out our Advanced Swift bootcamp.
Self
acts like an associated type. Unlike other associated types, you do not get to choose the type associated with Self
. Self
is automatically associated with the type implementing the protocol.
But the error message talks about a protocol that “has Self or associated type requirements”. This makes it sound like they are different things.
This is hair-splitting. But a hair in the wrong place distracts. I went to find an answer. I have found it for you in the source code for the abstract syntax tree used by the Swift compiler. A doc comment on AssociatedTypeDecl
says:
Every protocol has an implicitly-created associated type ‘Self’ that
describes a type that conforms to the protocol.
Case closed: Self
is an associated type.
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...