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 Chapter 5, A Modern Paradigm – Closures and Functional Programming, we created a function that helped us find the first number 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset