Beginning Swift Programming Part 13 — Generics

Bob Roebling
Swift2Go
Published in
11 min readApr 22, 2018
Photo by Martin Reisch on Unsplash

Previously we covered type aliases, property observers, and Self vs self.

About the time I tried talking about Self, I realized we were going to need to cover generics before Self could really be explained well.

It’s also a bit paradoxical because it’s difficult to explain generics without Self. Protocols can also use Self to really expand on their functionality. This being said, most articles go straight for generics or protocols and end up explaining all three all at once.

This makes it easier on the author who already understands the relationships, hard on the reader who is just learning one of the Swift features. This seems to be a common theme across technology, possibly in other fields. The easier it is on the developer or admin, the harder it is on the user and vice versa. The difficulty is constant but the burden of the task is split between each group. The best-written apps are usually the ones where the developer takes the difficult path.

Generics

While we have been writing code, have you ever thought “It sure would be nice if I didn’t have to type this same method to perform the same task for everything”? This is where generics come in.

Generics allow you to create reusable functions that can be used with different types as long as the type is compatible with the task being performed.

This means you could write a single function that could calculate tip amounts regardless of whether an int, double, or float was passed in to calculate the tip. It works for any Binary Integer type, but not so well with String types. Let’s take a look at our first generic function:

So we have some new syntax for you to look at. First is <T>. This could be anything inside of the brackets, most commonly we just use T to denote some Type, you might also see this written as <Element>.

The next thing you might notice is the function declaration changed a little: mutating func add<T>(newItem: T). This just reads as new mutating func called add which uses a generic type named <T> and takes a single argument newItem of type T. mutating means this function can alter, or mutate, the structures items array.

The type is inferred from whatever type the List was initially instantiated with. As soon as the compiler saw this line var integerList = List<Int>(), it changed all occurrences of <T> within the scope of the generic List structure to <Int>. Therefore the items array and all functions expected a type of Int.

If I created a new List using <String> and stored it in stringList, the array and functions would expect a type of String to be used.

You’ve probably seen this syntax before, like when we create dictionaries and arrays. We’ve been writing these shorthand, heavily relying on type inference, but behind the scenes Swift has come behind us and automatically expanded those types for us.

Integer arrays are declared using the syntax [Int](), but swift expands this to Array<Int>()

String dictionaries are declared using the syntax [String: String](), but just like arrays Swift understand dictionaries as Dictionary<String: String>().

With that syntax, we can determine that dictionaries and arrays are both generic types, they don’t care about what they are set to, they just care that any value that is used within them conforms to the type they are declared as.

So lets look at that second method. func getItem<T>(at index: Int) -> T?. It works in the same way as add, swappingT to whatever type was used to create our List. But it uses Int as the parameter value. The reason for this, is becuase we need the integer-based index of the array. Even though the array’s contents are generic, the index is still an int.

Of course the element at that index will be our generic type, but what if the user hasn’t added anything to the array? We need to handle that situation just in case the element doesn’t exist. So we first check to make sure we have an item in our list. If we do, then we can try to get at that element. If we can’t get that element we just return nil.

Notice I said try. There is another issue with the logic and if you’ve been practicing you’ll know what it is. I’ll give you a hint. What if we add our three items to the array as we did above, but then perform the following statement:
let value = integerList.getItem(at: 3)? Seems like we need to check for something else to keep our program from crashing. I’ll let you figure out how to do that.

Here’s another question for you. If we created a new list using
var doubleList = List<Double>(), added the values 3, 4 , and 5 to our list, then used let value = doubleList.getElement(at: 0). What is the exact text that would display in the console when you used print(value)?

Photo by Katie Montgomery on Unsplash

Conformance

You can have generics conform to specific protocols so they can only be instanciated with specific types. For example, when you use the protocol BinaryInteger, you are saying only signed (+/-) and unsigned (+) integers can use this generic method.

You can conform to any type but it’s best used with basic behaviors and base protocols such as Numeric, Stridable, Sequence, and/or Collection.

There are basic behaviors that you can conform to, a full list is found here, but I will cover some of the most commonly used.

  • Equatable — ability to check if the value of one variable is equal to the value of another.
  • Comparable — ability to compare the value of one variable to another using relational (Boolean) operators. (e.g. greater than, less than, equal to)
  • Hashable — creates an integer hash value to allow you use the type in a set or as a dictionary key.

Sometimes one will suffice for what you need, but other times, you may need to inherit from more than one.

For the best use of generics, you should look at the various protocols described in Apple’s documentation. Any protocol can be adopted, just make sure it makes sense with what you are trying to do. i.e. Don’t make a list of ages adopt the FloatingPoint protocol unless you are wanting to use the function with floating point (float, double) numbers.

Let’s take a look at how we can constrain a generic function to allow us to restrict what types can be used.

So by using the Numeric protocol, we tell the compiler, accept any type that is a number in replacement of T. This allows us to use the same function for multiple types.

It may not seem like much to be able to do something as simple as add two values together, but it does have power when you start doing more advanced techniques. The use of generics in a function format like this is one of those Swift features that isn’t utilized by new developers very much in their first projects. You can even write an entire app without creating generics, but one day you’ll come across a scenario where you will want to use the same function for two different data types. Now you just need to find the protocol that you would need to conform to and you can turn that strongly typed function into a generic function and re-use it as many times as you need in whichever type you need.

Generic in Protocols

This is that point in time where I was trying not to talk about generic protocols but wanted to talk about conformance and the examples I kept thinking of required generics in protocols.

Generics in protocols work as you’d expect them to, but conformance goes a long way with this. This also gives me a chance to talk about Self a little more as well as a close cousin of typealias called associatedtype.

First lets cover Self.

This is a binary search algorithm used in Apple’s WWDC 2015 video, the way binary search works is much like you’d go through a dictionary or phone book. Let’s say you are looking for the word “Swift” in the dictionary.

  1. First you open the book and you end up somewhere in the “M”s.
  2. “S” is greater than “M” so you flip halfway to the back of the book and you end up somewhere in the “T”s.
  3. “S” is less than “T” so you flip halfway back between “M” and “T”.
  4. You keep doing this until you land on the page with “Swift”.

In this example, we are providing a protocol that can be used with any type as long as the type is equatable to Self. Self in this context means we want to make sure the value that is passed in is also able to conform to the Ordered protocol. We have a number struct which is what we will use to compare a value of the same type.

In the binary search we pass in an array of anything that adopted the Ordered protocol, the key will be a single value that adopted the Ordered protocol.

We then get the high and low index of the array (since they are ordered), and proceed to find the value we are looking for. We first get the middle of the array. The algorithm lo + (hi — lo) / 2 does this for us. 0 + (10 - 0) / 2 = 5. Later on if we go high 5 + (10–5) / 2 = 8 (rounded up because 5 / 2 = 2.5 which is rounded to 3).

Next we check if sortedKeys[mid] which is getting the value 5, precedes(k) or is before the value we are looking for, then we will set lo = mid + 1 or one higher than the middle we just checked. If it’s not then we set hi = mid because we want to check everything lower than the mid.

And this will keep going until there is only one value left which becomes the lo.

Associated types are used as placeholders similar to <T> but are used in the protocol declaration, I’ll re-write the List example from above using a protocol with an associated type

First, we have our associatedtype, we will just call this Item because it makes sense to have items in an array.

Next we create our items array using a getter denoted by { get }. This tells the compiler this should be read only. If we wanted it to be readable and writeable we could use { get set }. In this case we only want the user to set the variable using the function add. I will talk more about getters and setters in a future article.

Again we have a mutating func because the function mutates itself, i.e. the struct that owns the method. And we create the method for getting items, nothing new here.

The List<T> struct, outside of the protocol adoption mostly stays the same as before. Of course, I didn’t create an extension for the protocol, which could have been done if we wanted to include default functionality, but sometimes when going between types, you might want different functionality. For example, with a List<String> do you want to append character arrays or string arrays? What about a List<Character>?

With our new knowledge that other protocols exist that common types such as String, Int, Double, etc… inherit from like Numeric, we could make an extension of Numeric that adopts the protocol and set up default functionality that would encompass all Numeric types. Then we could create one for StringProtocol types for Strings.

There is one last thing I want to talk about with generics and that is the where clause. The where clause is an amendment to protocols or associatedtypes.

This states that myProtocol places a requirement on anything that adopts this protocol to also adopt Hashable. Typically conformance to Swift standard library protocols requires you to implement a few associated types, variables and/or methods that look a bit funky. In the case of hashable, you must add the following to your Struct or Class.

hashvalue is pretty straight-forward, butstatic func ==(lhs:rhs:) -> Bool is all new for us.

static just means this can be called anywhere just by using ListA == ListB and compares the two Lists for equality. The == is where the equality comes from, it’s just adopting the operator in a way. lhs and rhs stand for left-hand side and right-hand side of the equality operator.

We are returning a Boolean but the implementation of the function is empty. So what goes in there? The logic you plan to use to check for equality. I just made a default implementation and it looks like this in context:

If lhs is equal to rhs we return true, otherwise false.

For the second part, the where clause with an associated type is when you want to provide functionality if the object that adopted the protocol also adopts the associated type’s requirement. If the object does, it get’s all methods that use the associated type, otherwise it doesn’t. The implementation might look something like this:

In short the difference of the where clause between the protocol level and the associated type level is this: When used at the protocol level, the adopting object is required to adopt the protocol referenced in the where clause. When used at the associated type level, the object is not required to adopt the protocol defined in the where clause but will not get access to all of the protocols available methods if it does not.

Summary

That about wraps up generics. They provide a lot of functionality with only a few changes to your code. It’s definitely something to keep in mind when you create your protocols. “Will this protocol be used for different types?”, “Are the types so different that I should look using the where clause to limit what functionality is provided?”

Hopefully I was able to teach you something about generics today. For more information you can take a look at Apple’s documentation on generics.

What’s Next

The good news is the topics are almost over. This was a bit more difficult to write about because usually the cases where you’d use generics greatly differ. I think that this probably needs more discussion and I have a great use case for generics that can be used in many places, but before we talk about that, I need to cover one more topic.

Up next is Error Handling.

In programming, most people think about the driving logic behind how your app works to be the most important thing. While the logic is important, more important are things such as code comments, maintaining readabililty, consistency in file layouts and code structure, and finally how you handle errors in your code.

Anyone can make an app that does something neat, but how good is your app when the server API changes and you get data that you weren’t expecting? Your app will crash if you don’t handle the change properly. Because of the criticality of error handling, I’ll show you the iOS and macOS way of handling these errors because, yes, it is that important to notify your user when an error occurred.

Until then, practice writing your own generics, if you get stuck refer back to the documentation. Don’t stress if you don’t get it at first, generics take time.

--

--

Bob Roebling
Swift2Go

Bob is a Senior Infrastructure Administrator and tech evangelist with a background in multiple programming languages.