This chapter would not be complete if we didn't address some very useful features from Swift. Tuples are very useful types that let you return multiple objects as one, without a strongly typed wrapper. Aliases let you quickly define simple type shortcuts. Finally, we'll cover the basics of generics. While generics could be covered in a whole book, we'll just scratch the surface of their syntax, features, and limits, as we'll make use of them extensively throughout this book.
Tuples are used to represent a group of values as a single value. Tuples cannot conform to protocols, nor can they inherit. They cannot declare functions in the same way that we can declare a function on a struct
or a class
. They may look limited, but they have their place as first-class types in the language.
Tuples can hold any number of values, from any number of types. You can declare a tuple with the same types—let's say a 2D point in Double
:
let origin = (0.0, 0.0)
You can also name the parameters, as follows:
let point = (x: 10.0, y: 10.0)
The two forms are equivalent, but you may want to use the named version, for readability reasons. If you're referencing a size, for example, the tuple would more accordingly be named (width: Double, height: Double)
. For obvious reasons, this helps to provide a better understanding of your code.
There is a simple method to access tuple values. Take, for example, the size
pair, as follows:
let size = (width: 200, height: 400) let (w, h) = size let (width, _) = size
In the preceding example, we initialize a tuple on the first line. On the second line, we destructure both parameters as w
and h
. On the last line is what we call a partial destructuring: when you're only interested in one part of the tuple, you can extract only a part of it. This is useful when dealing with large tuples.
Tuples are first-class citizens in Swift; you can use them, like any other type, as function parameters. The following code demonstrates how to declare a simple function that computes to the Euclidean distance between two points, a
and b
, represented by tuples:
func distance(_ a: (Double, Double), _ b: (Double, Double)) -> Double { returnsqrt(pow(b.0 - a.0, 2) + pow(b.1 - a.1, 2)) } distance(point, origin) == 5.0
You may have noticed that the named parameters of the point
tuple are ignored in this case; any pair of Double
will be accepted in the method, no matter what they are named.
The opposite is true, as well:
func slope(_ a: (x: Double, y: Double),_ b: (x: Double, y: Double)) -> Double { return (b.y - a.y) / (b.x - a.x) } slope((10, 10), (x: 1, y: 1)) == 1
We've seen examples of using tuples with the same types, but remember that, tuples can contain any type, and as many values as you wish.
Type aliases are a simple addition to the language; they let you reference simple or complex types by an alias. They support all declarations that you can imagine, from the simplest to the most complex.
The following block contains declarations for aliasing the following:
- A string class into a
MyString
- A function declaration into a
Block
- A block that takes any argument and returns any value
- A block that takes no argument and returns any value
Let's see the code block; they let you:
typealias MyString = String typealias Block = () -> Void typealias TypedBlock<T, U> = (T) -> U typealias ReturningBlock<U> = () -> U
We could have also defined Block
in the function of ReturningBlock
:
typealias Block = ReturningBlock<()>
You can also use type aliases for protocol compositions and complex types, as follows:
- You can declare a type that conforms to a protocol and is of a particular class
- You can delete a type that conforms to multiple protocols
Let's see an example, as follows:
protocol SomeProtocol {} protocol OtherProtocol {} typealias ViewControllerProtocol = NSViewController & SomeProtocol typealias BothProtocols = SomeProtocol & OtherProtocol
You will often find yourself using type aliases, in order to make your code more readable and more expressive. They are a powerful tool for hiding away some of the implementation complexity or verbosity when declaring long conformances. With type aliases, you can be encouraged to craft many protocols, each with a very small requirement list; then, you can compose all of those protocols when you need them, expressed as those types.
Generics is a complex subject, and would likely require a full book of its own, for extensive coverage extensively. For the purpose of this book, we'll provide a quick refresher on generics, covering the basics that are required to understand the constructions that we'll use in the different design patterns presented in the next chapters.
In Swift, the simplest form of generics would be the generics in functions. You can use generics very simply, with angled brackets, as follows:
func concat<T>(a: T, b: T) -> [T] { return [a,b] }
The concat
method knows nothing about the types that you are passing in, but generics gives us many guarantees over using Any
:
a
andb
should be of the same type- The
return
type is an array of elements that have the same type asa
andb
- The type is inferred from the context so you don't have to type it in when you code
You can also leverage protocol conformance in your generic functions, as follows:
protocol Runnable { func run() } func run<T>(runnable: T) where T: Runnable { runnable.run() }
In this case, the method that is run can only be called with an object that is Runnable
.
You can also make complex types generic. In our example, we created this wrapper around a list of Runnable
, called ManyRunner
. The job of a many runner is to run all of the runnables. The ManyRunner
is itself Runnable
, so we have created a kind of type recursion, as follows:
struct ManyRunner<T>: Runnable where T: Runnable { let runnables: [T] func run() { runnables.forEach { $0.run() } } }
Let's also provide a base object that runs a simple Incrementer
. Each time the Incrementer
is run, the static count will increment, to keep track of the number of invocations:
struct Incrementer: Runnable { private(set) static var count = 0 func run() { Incrementer.count += 1 } }
When using generics on types, remember that the types have to be the same:
// This works let runner = ManyRunner(runnables: [Incrementer(),Incrementer()]) runner.run() assert(Incrementer.count == 2) // runner is of type ManyRunner<Incrementer> ManyRunner(runnables: [Incrementer(), Runners(runnables: [Incrementer()])] as [Runnable]).run() // This produces the following compile error // In argument type '[Runnable]', 'Runnable' does not conform to expected type 'Runnable'
We'll look at how to overcome these limitations in Chapter 8, Swift-Oriented Patterns.
You can also use associated types in your protocols. These associated types let you define protocols that are generics, like this: RunnableWithResult
. We can implement a bunch of logic and code around the run()
method, without actually knowing anything about the return types. We'll encounter this construction many times in this book, so it's important that you're comfortable with associate types:
protocol RunnableWithResult { associatedtype ResultType func run() -> ResultType } struct RunnersWithResult<T>: RunnableWithResult where T: RunnableWithResult { let runnables: [T] func run() -> [T.ResultType] { return runnables.map { $0.run() } } }
Like with generic types, you can't mix and match heterogeneous types. The following example will not compile; later in this book, you'll see strategies for overcoming this common problem when dealing with generics:
struct IntRunnable { func run() -> Int { return 0 } } struct StringRunnable { func run() -> String { return "OK" } } let runnables: [RunnableWithResult] = [StringRunnable(), IntRunnable()]
This will yield the following dreaded error:
Protocol 'RunnableWithResult' can only be used as a generic constraint because it has Self or associated type requirements