Chapter 6. Make Swift Work For You – Protocols and Generics
As we learned in Chapter 2, Building Blocks – Variables, Collections, and Flow Control, Swift is a strongly typed language, which means that every piece of data must have a type. Not only can we take advantage of this to reduce the clutter in our code, we can also leverage it to let the compiler catch bugs for us. The earlier we catch a bug, the better. Besides not writing them in the first place, the earliest place where we can catch a bug is when the compiler reports an error.
Two big tools that Swift provides to achieve this are called protocols and generics. Both of them use the type system to make our intentions clear to the compiler so that it can catch more bugs for us.
In this chapter, we will cover the following topics:
- Protocols
- Generics
- Extending existing generics
- Extending protocols
- Putting protocols and generics to use
Protocols
The first tool we will look at is protocols. A protocol is essentially a contract that a type can sign, specifying that it will provide a certain interface to other components. This relationship is significantly looser than the relationship a subclass has with its superclass. A protocol does not provide any implementation to the types that implement them. Instead, a type can implement them in any way that they like.
Let's take a look at how we define a protocol, in order to understand them better.
Defining a protocol
Let's say we have some code that needs to interact with a collection of strings. We don't actually care what order they are stored in and we only need to be able to add and enumerate elements inside the container. One option would be to simply use an array, but an array does way more than we need it to. What if we decide later that we would rather write and read the elements from the file system? Furthermore, what if we want to write a container that would intelligently start using the file system as it got really large? We can make our code flexible enough to do this in the future by defining a string container protocol, which is a loose contract that defines what we need it to do. This protocol might look similar to the following code:
protocol StringContainer { var count: Int { get } mutating func addString(string: String) func enumerateStrings(handler: (string: String) -> Void) }
Predictably, a protocol is defined using the protocol
keyword, similar to a class or a structure. It also allows you to specify computed properties and methods. You cannot declare a stored property because it is not possible to create an instance of a protocol directly. You can only create instances of types that implement the protocol. Also, you may notice that none of the computed properties or methods provide implementations. In a protocol, you only provide the interface.
Since protocols cannot be initialized on their own, they are useless until we create a type that implements them. Let's take a look at how we can create a type that implements our StringContainer
protocol.
Implementing a protocol
A type "signs the contract" of a protocol in the same way that a class inherits from another class except that structures and enumerations can also implement protocols:
struct StringBag: StringContainer { // Error: Type 'StringBag' does not conform to protocol 'StringContainer' }
As you can see, once a type has claimed to implement a specific protocol, the compiler will give an error if it has not fulfilled the contract by implementing everything defined in the protocol. To satisfy the compiler, we must now implement the count
computed property, mutating function addString:
, and function enumerateStrings:
as they are defined. We will do this by internally holding our values in an array:
struct StringBag: StringContainer { var strings = [String]() var count: Int { return self.strings.count } mutating func addString(string: String) { self.strings.append(string) } func enumerateStrings(handler: (string: String) -> Void) { for string in self.strings { handler(string: string) } } }
The count
property will always just return the number of elements in our strings
array. The addString:
method can simply add the string to our array. Finally, our enumerateString:
method just needs to loop through our array and call the handler with each element.
At this point, the compiler is satisfied that StringBag
is fulfilling its contract with the StringContainer
protocol.
Now, we can similarly create a class that implements the StringContainer
protocol. This time, we will implement it using an internal dictionary instead of an array:
class SomeSuperclass {} class StringBag2: SomeSuperclass, StringContainer { var strings = [String:Void]() var count: Int { return self.strings.count } func addString(string: String) { self.strings[string] = () } func enumerateStrings(handler: (string: String) -> Void) { for string in self.strings.keys { handler(string: string) } } }
Here we can see that a class can both inherit from a superclass and implement a protocol. The superclass always has to come first in the list, but you can implement as many protocols as you want, separating each one with a comma. In fact, a structure and enumeration can also implement multiple protocols.
With this implementation we are doing something slightly strange with the dictionary. We defined it to have no values; it is simply a collection of keys. This allows us to store our strings without any regard to the order they are in.
Now, when we create instances, we can actually assign any instance of any type that implements our protocol to a variable that is defined to be our protocol, just like we can with superclasses:
var someStringBag: StringContainer = StringBag() someStringBag.addString("Sarah") someStringBag = StringBag2() someStringBag.addString("Sarah")
When a variable is defined with our protocol as its type, we can only interact with it using the interface that the protocol defines. This is a great way to abstract implementation details and create more flexible code. By being less restrictive on the type that we want to use, we can easily change our code without affecting how we use it. Protocols provide the same benefit that superclasses do, but in an even more flexible and comprehensive way, because they can be implemented by all types and a type can implement an unlimited number of protocols. The only benefit that superclasses provide over protocols is that superclasses share their implementations with their children.
Using type aliases
Protocols can be made more flexible using a feature called type aliases. They act as a placeholder for a type that will be defined later when the protocol is being implemented. For example, instead of creating an interface that specifically includes strings, we can create an interface for a container that can hold any type of value, as shown:
protocol Container { typealias Element mutating func addElement(element: Element) func enumerateElements(handler: (element: Element) -> Void) }
As you can see, this protocol creates a type alias called Element
using the keyword typealias
. It does not actually specify a real type; it is just a placeholder for a type that will be defined later. Everywhere we have previously used a string, we simply refer to it as Element
.
Now, we can create another string bag that uses the new Container
protocol with a type alias instead of the StringContainer
protocol. To do this, we not only need to implement each of the methods, we also need to give a definition for the type alias, as shown:
struct StringBag3: Container { typealias Element = String var elements = [Element:Void]() var count: Int { return elements.count } mutating func addElement(element: Element) { self.elements[element] = () } func enumerateElements(handler: (element: Element) -> Void) { for element in self.elements.keys { handler(element: element) } } }
With this code, we have specified that the Element
type alias should be a string for this implementation using an equal sign (=
). This code continues to use the type alias for all of the properties and methods, but you can also use string since they are in fact the same thing now.
Using the type alias actually makes it really easy for us to create another structure that can hold integers instead of strings:
struct IntBag: Container { typealias Element = Int var elements = [Element:Void]() var count: Int { return elements.count } mutating func addElement(element: Element) { self.elements[element] = () } func enumerateElements(handler: (element: Element) -> Void) { for element in self.elements.keys { handler(element: element) } } }
The only difference between these two pieces of code is that the type alias has been defined to be an integer in the second case instead of a string. We could use copy and paste to create a container of virtually any type, but as usual, doing a lot of copy and paste is a sign that there is a better solution. Also, you may notice that our new Container
protocol isn't actually that useful on its own because with our existing techniques, we can't treat a variable as just a Container
. If we are going to interact with an instance that implements this protocol, we need to know what type it has assigned the type alias to.
Swift provides a tool called generics to solve both of these problems.
Generics
A generic is very similar to a type alias. The difference is that the exact type of a generic is determined by the context in which it is being used, instead of being determined by the implementing types. This also means that a generic only has a single implementation that must support all possible types. Let's start by defining a generic function.
Generic function
In an array of numbers that passes a test:
func firstInNumbers( numbers: [Int], passingTest: (number: Int) -> Bool ) -> Int? { for number in numbers { if passingTest(number: number) { return number } } return nil }
This would be great if we only ever dealt with arrays of integers
, but clearly it would be helpful to be able to do this with other types. In fact, dare I say, all types? We achieve this very simply by making our function generic. A generic function is declared similar to a normal function, but you include a list of comma-separated placeholders inside angled brackets (<>
) at the end of the function name, as shown:
func firstInArray<ValueType>( array: [ValueType], passingTest: (value: ValueType) -> Bool ) -> ValueType? { for value in array { if passingTest(value: value) { return value } } return nil }
In this function, we have declared a single placeholder called ValueType
. Just like with type aliases, we can continue to use this type in our implementation. This will stand in for a single type that will be determined when we go to use the function. You can imagine inserting String
or any other type into this code instead of ValueType
and it would still work.
We use this function similarly to any other function, as shown:
var strings = ["This", "is", "a", "sentence"] var numbers = [1, 1, 2, 3, 5, 8, 13] firstInArray(strings, passingTest: {$0 == "a"}) // "a" firstInArray(numbers, passingTest: {$0 > 10}) // 13
Here, we have used firstInArray:passingTest:
with both an array of strings and an array of numbers. The compiler figures out what type to substitute in for the placeholder based on the variables we pass into the function. In the first case, strings
is an array of String
. It compares that to [ValueType]
and assumes that we want to replace ValueType
with String
. The same thing happens with our Int
array in the second case.
So what happens if the type we use in our closure doesn't match the type of array we pass in?
firstInArray(numbers, passingTest: {$0 == "a"}) // Cannot convert // value of type '[Int]' to expected argument type'[_]'
As you can see, we get an error that the types don't match.
You may have noticed that we have actually used generic functions before. All of the built in functions we looked at in Chapter 5, A Modern Paradigm – Closures and Functional Programming, such as map
and filter
are generic; they can be used with any type.
We have even experienced generic types before. Arrays and dictionaries are also generic. The Swift team didn't have to write a new implementation of array and dictionary for every type that we might want to use inside the containers; they created them as generic types.
Generic type
Similar to a generic function, a generic type is defined just like a normal type but it has a list of placeholders at the end of its name. Earlier in this chapter, we created our own containers for strings and integers
. Let's make a generic version of these containers, as shown:
struct Bag<ElementType> { var elements = [ElementType]() mutating func addElement(element: ElementType) { self.elements.append(element) } func enumerateElements( handler: (element: ElementType) -> () ) { for element in self.elements { handler(element: element) } } }
This implementation looks similar to our type alias versions, but we are using the ElementType
placeholder instead.
While a generic function's placeholders are determined when the function is called, a generic type's placeholders are determined when initializing new instances:
var stringBag = Bag(elements: ["This", "is", "a", "sentence"]) var numberBag = Bag(elements: [1, 1, 2, 3, 5, 8, 13])
All future interactions with a generic instance must use the same types for its placeholders. This is actually one of the beauties of generics where the compiler does work for us. If we create an instance of one type and accidently try to use it as a different type, the compiler won't let us. This protection does not exist in many other programming languages, including Apple's previous language: Objective-C.
One interesting case to consider is if we try to initialize a bag with an empty array:
var emptyBag = Bag(elements: []) // Cannot invoke initilaizer for // type 'Bag<_>' with an argument list of type '(elements: [_])'
As you can see, we get an error that the compiler could not determine the type to assign to our generic placeholder. We can solve this by giving an explicit type to the generic we are assigning it to:
var emptyBag: Bag<String> = Bag(elements: [])
This is great because not only can the compiler determine the generic placeholder types based on the variables we pass to them, it can also determine the type based on how we are using the result.
We have already seen how to use generics in a powerful way. We solved the first problem we discussed in the type alias section about copying and pasting a bunch of implementations for different types. However, we have not yet figured out how to solve the second problem: how do we write a generic function to handle any type of our Container
protocol? The answer is that we can use type constraints.
Type constraints
Before we jump right into solving the problem, let's take a look at a simpler form of type constraints.
Protocol constraints
Let's say that we want to write a function that can determine the index of an instance within an array using an equality check. Our first attempt will probably look similar to the following code:
func indexOfValue<T>(value: T, inArray array: [T]) -> Int? { var index = 0 for testValue in array { if testValue == value { // Error: Cannot invoke '==' return index } index++ } return nil }
With this attempt, we get an error that we cannot invoke the equality operator (==
). This is because our implementation must work for any possible type that might be assigned to our placeholder. Not every type in Swift can be tested for equality. To fix this problem, we can use a type constraint to tell the compiler that we only want to allow our function to be called with types that support the equality operation. We add type constraints by requiring the placeholder to implement a protocol. In this case, Swift provides a protocol called Equatable
, which we can use:
func indexOfValue<T: Equatable>( value: T, inArray array: [T] ) -> Int? { var index = 0 for testValue in array { if testValue == value { return index } index++ } return nil }
A type constraint looks similar to a type implementing a protocol using a colon (:
) after a placeholder name. Now, the compiler is satisfied that every possible type can be compared using the equality operator. If we were to try to call this function with a type that is not equatable, the compiler would produce an error:
class MyType {} var typeList = [MyType]() indexOfValue(MyType(), inArray: typeList) // Cannot convert value of type '[MyType]' to expected // argument type '[_]'
This is another case where the compiler can save us from ourselves.
We can also add type constraints to our generic types. For example, if we tried to create a bag with our dictionary implementation without a constraint, we would get an error:
struct Bag2<ElementType> { var elements: [ElementType:Void] // Type 'ElementType' does not conform to protocol 'Hashable' }
This is because the key of dictionaries has a constraint that it must be Hashable
. Dictionary is defined as struct Dictionary<Key : Hashable, Value>
. Hashable
basically means that the type can be represented using an integer. In fact, we can look at exactly what it means if we write Hashable
in Xcode and then click on it while holding down the Command Key. This brings us to the definition of Hashable
, which has comments that explain that the hash value of two objects that are equal must be the same. This is important to the way that Dictionary
is implemented. So, if we want to be able to store our elements as keys in a dictionary, we must also add the Hashable
constraint:
struct Bag2<ElementType: Hashable> { var elements: [ElementType:Void] mutating func addElement(element: ElementType) { self.elements[element] = () } func enumerateElements( handler: (element: ElementType) -> () ) { for element in self.elements.keys { handler(element: element) } } }
Now the compiler is happy and we can start to use our Bag2
struct with any type that is Hashable
. We are close to solving our Container
problem, but we need a constraint on the type alias of Container
, not Container
itself. To do that, we can use a where
clause.
Where clauses for protocols
You can specify any number of where
clauses you want after you have defined each placeholder type. They allow you to represent more complicated relationships. If we want to write a function that can check if our container contains a particular value, we can require that the element type is equatable:
func container<C: Container where C.Element: Equatable>( container: C, hasElement element: C.Element ) -> Bool { var hasElement = false container.enumerateElements { testElement in if element == testElement { hasElement = true } } return hasElement }
Here, we have specified a placeholder C
that must implement the Container
protocol; it must also have an Element
type that is Equatable
.
Sometimes we may also want to enforce a relationship between multiple placeholders. To do that, we can use an equality test inside the where
clauses.
Where clauses for equality
If we want to write a function that can merge one container into another while still allowing the exact types to vary, we could write a function that would require that the containers hold the same value:
func merged<C1: Container, C2: Container where C1.Element == C2.Element>( lhs: C1, rhs: C2 ) -> C1 { var merged = lhs rhs.enumerateElements { element in merged.addElement(element) } return merged }
Here, we have specified two different placeholders: C1
and C2
. Both of them must implement the Container
protocol and they must also contain the same Element
type. This allows us to add elements from the second container into a copy of the first container that we return at the end.
Now that we know how to create our own generic functions and types, let's take a look at how we can extend existing generics.
Extending generics
The two main generics that we will probably want to extend are arrays and dictionaries. These are the two most prominent containers provided by Swift and are used in virtually every app. Extending a generic type is simple once you understand that an extension itself does not need to be generic.
Adding methods to all forms of a generic
Knowing that an array is declared as struct Array<Element>
, your first instinct to extend an array might look something similar to this:
extension Array<Element> { // Use of undeclared type 'Element' // ... }
However, as you can see, you would get an error. Instead, you can simply leave out the placeholder specification and still use the Element
placeholder inside your implementations. Your other instinct might be to declare Element
as a placeholder for your individual methods:
extension Array { func someMethod<Element>(element: Element) { // ... } }
This is more dangerous because the compiler doesn't detect an error. This is wrong because you are actually declaring a new placeholder Element
to be used within the method. This new Element
has nothing to do with the Element
defined in Array
itself. For example, you might get a confusing error if you tried to compare a parameter to the method to an element of the Array:
extension Array { mutating func addElement<Element>(element: Element) { self.append(element) // Cannot invoke 'append' with argument list // of type '(Element)' } }
This is because the Element
defined in Array
cannot be guaranteed to be the exact same type as the new Element
defined in addElement:
. You are free to declare additional placeholders in methods on generic types, but it is best to give them unique names so that they don't hide the type's version of the placeholder.
Now that we understand this, let's add an extension to the array that allows us to test if it contains an element passing a test:
extension Array { func hasElementThatPasses( test: (element: Element) -> Bool ) -> Bool { for element in self { if test(element: element) { return true } } return false } }
As you can see, we continue to use the placeholder Element
within our extension. This allows us to call the passed in test closure for each element in the array. Now, what if we want to be able to add a method that will check if an element exists using the equality operator? The problem that we will run into is that array does not place a type constraint on Element
requiring it to be Equatable
. To do this, we can add an extra constraint to our extension.
Adding methods to only certain instances of a generic
A constraint on an extension is written as a where
clause, as shown:
extension Array where Element: Equatable { func containsElement(element: Element) -> Bool { for testElement in self { if testElement == element { return true } } return false } }
Here we add a constraint that guarantees that our element is equatable. This means that we will only be able to call this method on arrays that have equatable elements:
[1,2,3,4,5].containsElement(4) // true class MyType {} var typeList = [MyType]() typeList.containsElement(MyType()) // Type 'MyType' does not // conform to protocol 'Equtable'
Again, Swift is protecting us from accidently trying to call this method on an array that it wouldn't work for.
These are the building blocks that we have to play with generics. However, we actually have one more feature of protocols that we have not discussed, which works really well in combination with generics.
Extending protocols
We first discussed how we can extend existing types in Chapter 3, One Piece at a Time – Types, Scopes, and Projects. In Swift 2, Apple added the ability to extend protocols. This has some fascinating implications, but before we dive into those, let's take a look at an example of adding a method to the Comparable
protocol:
extension Comparable { func isBetween(a: Self, b: Self) -> Bool { return a < self && self < b } }
This adds a method to all types that implement the Comparable. This means that it will now be available on any of the built-in types that are comparable and any of our own types that are comparable:
6.isBetween(4, b: 7) // true "A".isBetween("B", b: "Z") // false
This is a really powerful tool. In fact, this is how the Swift team implemented many of the functional methods we saw in Chapter 5, A Modern Paradigm – Closures and Functional Programming. They did not have to implement the map method on arrays, dictionaries, or on any other sequence that should be mappable; instead, they implemented it directly on SequenceType
.
This shows that similarly, protocol extensions can be used for inheritance, and it can also be applied to both classes and structures and types can also inherit this functionality from multiple different protocols because there is no limit to the number of protocols a type can implement. However, there are two major differences between the two.
First, types cannot inherit stored properties from protocols, because extensions cannot define them. Protocols can define read only properties but every instance will have to redeclare them as properties:
protocol Building { var squareFootage: Int {get} } struct House: Building { let squareFootage: Int } struct Factory: Building { let squareFootage: Int }
Second, method overriding does not work in the same way with protocol extensions. With protocols, Swift does not intelligently figure out which version of a method to call based on the actual type of an instance. With class inheritance, Swift will call the version of a method that is most directly associated with the instance. Remember, when we called clean on an instance of our House subclass in Chapter 3, One Piece at a Time – Types, Scopes, and Projects, it calls the overriding version of clean, as shown:
class Building { // ... func clean() { print( "Scrub \(self.squareFootage) square feet of floors" ) } } class House: Building { // ... override func clean() { print("Make \(self.numberOfBedrooms) beds") print("Clean \(self.numberOfBathrooms) bathrooms") } } let building: Building = House( squareFootage: 800, numberOfBedrooms: 2, numberOfBathrooms: 1 ) building.clean() // Make 2 beds // Clean 1 bathroom
Here, even though the building variable is defined as a Building
, it is in fact a house; so Swift will call the house's version of clean. The contrast with protocol extensions is that it will call the version of the method that is defined by the exact type the variable is declared as:
protocol Building { var squareFootage: Int {get} } extension Building { func clean() { print( "Scrub \(self.squareFootage) square feet of floors" ) } } struct House: Building { let squareFootage: Int let numberOfBedrooms: Int let numberOfBathrooms: Double func clean() { print("Make \(self.numberOfBedrooms) beds") print("Clean \(self.numberOfBathrooms) bathrooms") } } let house = House( squareFootage: 1000, numberOfBedrooms: 2, numberOfBathrooms: 1.5 ) house.clean() // Make 2 beds // Clean 1.5 bathrooms (house as Building).clean() // Scrub 1000 square feet of floors
When we call clean on the house variable which is of type House
, it calls the house version; however, when we cast the variable to a Building
and then call it, it calls the building version.
All of this shows that it can be hard to choose between using structures and protocols or class inheritance. We will look at the last piece of that consideration in the next chapter on memory management, so we will be able to make a fully informed decision when moving forward.
Now that we have looked at the features available to us with generics and protocols, let's take this opportunity to explore some more advanced ways protocols and generics are used in Swift.
Putting protocols and generics to use
One cool part of Swift is generators and sequences. They provide an easy way to iterate over a list of values. Ultimately, they boil down to two different protocols: GeneratorType
and SequenceType
. If you implement the SequenceType
protocol in your custom types, it allows you to use the for-in loop over an instance of your type. In this section, we will look at how we can do that.
Generators
The most critical part of this is the GeneratorType
protocol. Essentially, a generator is an object that you can repeatedly ask for the next object in a series until there are no objects left. Most of the time you can simply use an array for this, but it is not always the best solution. For example, you can even make a generator that is infinite.
There is a famous infinite series of numbers called the Fibonacci sequence, where every number in the series is the sum of the two previous numbers. This is especially famous because it is found all over nature from the number of bees in a nest to the most pleasing aspect ratio of a rectangle to look at. Let's create an infinite generator that will produce this series.
We start by creating a structure that implements the GeneratorType
protocol. The protocol is made up of two pieces. First, it has a type alias for the type of elements in the sequence and second, it has a mutating method called next
that returns the next object in the sequence.
The implementation looks similar to this:
struct FibonacciGenerator: GeneratorType { typealias Element = Int var values = (0, 1) mutating func next() -> Element? { self.values = ( self.values.1, self.values.0 + self.values.1 ) return self.values.0 } }
We defined a property called values
that is a tuple representing the previous two values in the sequence. We update values
and return the first element of the tuple each time next
is called. This means that there will be no end to the sequence.
We can use this generator on its own by instantiating it and then repeatedly calling next inside a while loop:
var generator = FibonacciGenerator() while let next = generator.next() { if next > 10 { break } print(next) } // 1, 1, 2, 3, 5, 8
We need to set up some sort of a condition so that the loop doesn't go on forever. In this case, we break out of the loop once the numbers get above 10. However, this code is pretty ugly, so Swift also defines the protocol called SequenceType
to clean it up.
Sequences
SequenceType
is another protocol that is defined as having a type alias for a GeneratorType
and a method called generate
that returns a new generator of that type. We could declare a simple sequence for our FibonacciGenerator
, as follows:
struct FibonacciSequence: SequenceType { typealias Generator = FibonacciGenerator func generate() -> Generator { return FibonacciGenerator() } }
Every for-in loop operates on the SequenceType
protocol, so now we can use a for-in loop on our FibonacciSequence
:
for next in FibonacciSequence() { if next > 10 { break } print(next) }
This is pretty cool; we can easily iterate over the Fibonacci sequence in a very readable way. It is much easier to understand the preceding code than it would be to understand a complicated while loop that has to calculate the next value of the sequence each time. Imagine all of the other type of sequences we can design such as prime numbers, random name generators, and so on.
However, it is not always ideal to have to define two different types to create a single sequence. To fix this, we can use generics. Swift provides a generic type called AnyGenerator
with a companion function called anyGenerator:
. This function takes a closure and returns a generator that uses the closure as its next method. This means that we don't have to explicitly create a generator ourselves; instead we can use anyGenerator:
directly in a sequence:
struct FibonacciSequence2: SequenceType { typealias Generator = AnyGenerator<Int> func generate() -> Generator { var values = (0, 1) return anyGenerator({ values = (values.1, values.0 + values.1) return values.0 }) } }
In this version of FibonacciSequence
, we create a new generator every time generate is called that takes a closure that does the same thing that our original FibonacciGenerator
was doing. We declare the values
variable outside of the closure so that we can use it to store the state between calls to the closure. If your generator is simple and doesn't require a complicated state, using the AnyGenerator
generic is a great way to go.
Now let's use this FibonacciSequence
to solve the kind of math problem that computers are great at.
Product of Fibonacci numbers under 50
What if we want to know what is the result of multiplying every number in the Fibonacci sequence under 50? We can try to use a calculator and painstakingly enter in all of the numbers, but it is much more efficient to do it in Swift.
Let's start by creating a generic SequenceType
that will take another sequence type and limit it to stop the sequence once it has reached a maximum number. We need to make sure that the type of the maximum value matches the type in the sequence and also that the element type is comparable. For that, we can use a where clause on the element type:
struct SequenceLimiter< S: SequenceType where S.Generator.Element: Comparable >: SequenceType { typealias Generator = AnyGenerator<S.Generator.Element> let sequence: S let max: S.Generator.Element init(_ sequence: S, max: S.Generator.Element) { self.sequence = sequence self.max = max } func generate() -> Generator { var g = self.sequence.generate() return anyGenerator({ if let next = g.next() { if next <= self.max { return next } } return nil }) } }
Notice that when we refer to the element type, we must go through the generator type.
When our SequenceLimiter
structure is created, it stores the original sequence. This is so that it can use the result of its generate
method each time generate
is called on this parent sequence. Each call to generate
needs to start the sequence over again. It then creates an AnyGenerator
with a closure that calls next
on the locally initialized generator of the original sequence. If the value returned by the original generator is greater than or equal to the maximum value, we return nil
, indicating that the sequence is over.
We can even add an extension to SequenceType
with a method that will create a limiter for us:
extension SequenceType where Generator.Element: Comparable { func limit(max: Generator.Element) -> SequenceLimiter<Self> { return SequenceLimiter(self, max: max) } }
We use Self
as a placeholder representing the specific type of the instance the method is being called on.
Now, we can easily limit our Fibonacci sequence to only values under 50:
FibonacciSequence().limit(50)
The last part we need to solve our problem is the ability to find the product of a sequence. We can do this with another extension. In this case, we are only going to support sequences that contain Int
s so that we can ensure that the elements can be multiplied:
extension SequenceType where Generator.Element == Int { var product: Generator.Element { return self.reduce(1, combine: *) } }
This method takes advantage of the reduce function to start with the value one and multiply it by every value in the sequence. Now we can do our final calculation easily:
FibonacciSequence().limit(50).product // 2,227,680
Almost instantaneously, our program will return the result 2,227,680. Now we can really understand why we call these devices computers.
Summary
Protocols and generics are definitely complex, but we have seen that they can be used to effectively let the compiler protect us from ourselves. In this chapter, we have covered how protocols are like contracts for types to sign. We have also seen that protocols can be made more flexible using type aliases. Generics allow us to take full advantage of protocols with type aliases and also allow us to create powerful and flexible types that adapt to the contexts in which they are used. Finally, we looked at how we can use protocols and generics in the form of sequences and generators to solve a complex math problem in a very clean and understandable way, as an inspiration to solve other types of problems just as cleanly.
At this point we have covered all of the core features of the Swift language. We are now ready to look a little bit deeper at how data is actually stored while a program is run and how we can best manage the resources used by our programs.