Learning Swift - Generics
One of the nice things about Swift is how clean your code looks. A lot of the weird characters that pepper the code of other languages has been eliminated: No more semi-colons, asterisks etc.
But then you are reading somebody else’s code and you find these angle brackets all over the place and they don’t seem to make sense.
What does this mean?
func mid<T: Comparable>(array: [T]) -> T
It looks like it is a function to find the middle element in an array, but what
is <T: Comparable>
or [T]
or even just T
? They are describing Generic data
types.
Using the dictionary, I find the following definition for generic
:
characteristic of or relating to a class or group of things; not specific.
And that really provides quite a good definition for Swift’s generics too. They allow you to assign a non-specific data type to a function so that function can be used for many different data types.
But enough of the theory: by far the best way to understand generics is to encounter the problem they are meant to solve and then to solve it.
The other day I was processing data to feed to a charting library. I ended up with an array of CGFloats, but there were too many of them, so I wrote a function to give me the last x elements of an array of CGFloats:
func endOf(array: [CGFloat], numberOfElementsToInclude: Int) -> [CGFloat] {
if numberOfElementsToInclude > array.count { return array }
let startingElement = array.count - numberOfElementsToInclude
let endOfArray = Array(array[startingElement ..< array.count])
return endOfArray
}
let largeArray: [CGFloat] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
let smallArray = endOf(array: largeArray, numberOfElementsToInclude: 3)
//smallArray now equals [8.0, 9.0, 10.0]
OK, so largeArray
isn’t actually the largest array you have ever seen, but it
works for demo purposes. In my code, I had over 20,000 entries in the large
array and only wanted the last 5,000.
I was completely happy with that until I found another data source where the data was an array of Ints. Still perfectly chartable using my charting library, but I could not get the end of the array using my nice function. First thought was to replicate the function:
func endOf(array: [Int], numberOfElementsToInclude: Int) -> [Int] {
if numberOfElementsToInclude > array.count { return array }
let startingElement = array.count - numberOfElementsToInclude
let endOfArray = Array(array[startingElement ..< array.count])
return endOfArray
}
let largeArrayInt = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] // type inferred to be Int
let smallArrayInt = endOf(array: largeArrayInt, numberOfElementsToInclude: 7)
// smallArrayInt now equals [4, 5, 6, 7, 8, 9, 10]
A quick glance will reveal that the declaration line was the only thing that
changed. I didn’t have to give the function a new name because the compiler
worked out which of the two functions to use based on the types supplied. So all
I was doing was replacing CGFloat
with Int
in two places (the
numberOfElementsToInclude
parameter remains an Int
in both cases).
As soon as you find yourself replicating code and only changing the types of the data, that is a sign that you need to use generics.
For my first attempt at creating a generic function, all I did was replace the
word CGFloat
with T
where T
indicates some type that will be specified
when the function is called. The convention is to use single letters for generic
types: T
for Type, A
for AnyObject, Z
because you feel like it. The choice
is up to you.
That produced this error:
Use of undeclared type 'T'
. Well that seems fair enough… how is the compiler
supposed to know what a T
is?
It turns out that this is where the angle brackets come into play. You have to
warn the compiler that this function is going to get a generic type and that the
T
is only a placeholder, not a real type. So this version compiles and works -
notice the <T>
between the function name and the opening bracket:
func endOf<T>(array: [T], numberOfElementsToInclude: Int) -> [T] {
if numberOfElementsToInclude > array.count { return array }
let startingElement = array.count - numberOfElementsToInclude
let endOfArray = Array(array[startingElement ..< array.count])
return endOfArray
}
Now I have no code duplication and if I ever get another data set that uses a different object type, my function will still work.
Now we know to use generics instead of replicating code that operates in the same way on different data types.
And we know how to construct a generic function.
Using it is identical to using the two functions I had before:
let largeArray: [CGFloat] = [ 1.0, 2.0, 3.0, 4.0, 5.0,6.0, 7.0, 8.0, 9.0, 10.0 ]
let smallArray = endOf(array: largeArray, numberOfElementsToInclude: 3)
let largeArrayInt = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] // type inferred to be Int
let smallArrayInt = endOf(array: largeArrayInt, numberOfElementsToInclude: 7)
You do not have to specify what T
is when calling the function, that will be
inferred from the supplied data.
Moving on into more advanced use cases, we have two things to consider:
- Functions where the generic parameters must conform to a protocol.
- Functions that take generic parameters of different types.
The example function I started with (not mine but I forget where I got it… If it is yours, thanks you very much) used:
func mid <T: Comparable> (array: [T]) -> T
Instead of specifying the type as completely generic, it specified that it must
conform to the Comparable protocol. This is because the function body used a
sort()
function so the generic function needed to be sure that the elements in
the array could be compared. This technique can also be used to restrict the
parameter types to sub-classes of a certain class.
For a function with more than one generic data type, you need to use more than one placeholder letter:
func genericWithTwoDifferentTypes<T, U>(param1: T, param2: U) -> Bool {
// ...
}
And in exactly the same way, both T
and U
can be set to conform to a
protocol or be members of a certain super-class.
Hopefully this has taken some of the mystery out of generics, but if you have any questions, please get in touch using the Contact page , comment below or contact me via Twitter: @trozware .