Welcome to Swift Yeti! The blog that is all about the great ins and outs of Swift and Cocoa/CocoaTouch. We recieved a slurry of announcments this year at WWDC, one of the biggest being Swift.
Swift Yeti will be covering different topics and frameworks from Apple, by doing what we do best - getting our hands dirty and implementing some code.
The first topic of Swift Yeti . . . Generics!
Generics have been around for a long time so the concept is not very new, but for Cocoa programmers this is the beginning of something special. So what are Generics exactly? Generics allow a programmer to tell their functions and classes, "I am going to give you a type later and I want you to enforce that type everywhere I specify." This eliminates a whole slew of functions that you would have to write otherwise. What do I mean?
Well lets look at an example. I want to write a function that compares two values together.
func areIntsEqual(a: Int, b: Int) -> Bool { return a == b }
func areStringsEqual(a: String, b: String) -> Bool { return a == b }
These functions look identical, and while they are very tiny functions it would be great if I only had to write it once. Hello Generics.
func areValuesEqual<T: Equatable>(a: T, b: T) -> Bool { return a == b }
There we go. One function to handle a multitude of inputs.
There are two major ways Generics are implemented: type erasure and reified generics.
Type Erasure: The compiler will automatically downcast all types that have been specified.
(<T> => <AnyObject>, <K> => <AnyObject>)
Reified Generics: The compiler keeps type information.
(<T> => <T>, <K> => <K>)
A couple of problems arise when using type erasure, such as overloading. To show you one problem with type erasure look at the following example:
(written in Swift . . . cause it's a Swift blog)
func swap(typeA: <T>, typeB: <T>) { }
func swap(typeA: <K>, typeB: <K>) { }
When we try to compile, we would actually get this
func swap(typeA: <AnyObject>, typeB: <AnyObject>) { }
func swap(typeA: <AnyObject>, typeB: <AnyObject>) { }
We now have a conflicting method signature, which is not good.
In reified generics, this problem does not occur due to the maintained type. When we compile we still get this:
func swap(typeA: <T>, typeB: <T>) { }
func swap(typeA: <K>, typeB: <K>) { }
We get unique method signatures.
Not that this is the best example of great coding, but just as a showcase for one of the problems when using Type Erasure. There a lot of articles covering the differences between Type Erasure and Reified Generics, but on to Swift.
Generics in Swift:
As mentioned earlier, Swift generics are reified, so maintaining types at compile time is all good.
I spent some time with the Swift developers at WWDC and a lot has been done to optimize the Swift compiler. An interesting fact pulled from one of the conversations was Swift's ability to run generic code as generic code, except in places where the Swift compiler deems it faster to expand. The compiler will then expand out your generic code for each type used resulting in a unique method for each type. This is something we can see in C++ templates.
Let's look at a couple different places you can use generics in Swift.
Structs
Structs in Swift are very close to classes. In most cases they have the exact same functionality. Let's see how we can apply generics to a circular queue struct.
struct CircularQueue<T> {
let bufferSize: Int
var buffer: Dictionary<Int, T>
var currentIndex: Int
var oldestIndex: Int
init(bufferSize: Int) {
self.bufferSize = bufferSize
buffer = Dictionary<Int, T>(minimumCapacity: bufferSize)
currentIndex = 0
oldestIndex = 0
}
}
When defining a struct in Swift we start by using struct
followed by our struct name.
struct CircularQueue {}
To add generics to our class you use < >
after your struct name and specify a key for your generic type. This key can be anything from a simple letter T to a string MyType. It is just a placeholder for the type you will specifiy when constructing your struct elsewhere.
struct CircularQueue<T> {}
We can now use T anywhere in our function to specify type safety. In our example above we declared a Dictionary of type T.
As a side note you can apply protocols to your types. If we were going to use T as the key into a Dictionary it would have to conform to : Hashable
.
struct myStruct<T: Hashable> {
var myDictionary: Dictionary<T, Int>
}
Understandable since a Dictionary is implemented as a Hash Table. Doing this will enforce that T conforms to : Hashable
and ensures our code will never allow us to use a type that cannot be used as a key in a Dictionary.
Functions
Functions can also declare generic types.
func removeObject<T>(object: T, array: T[]) { }
In a very similar manner functions can specify a generic type within < >
and then proceed to use the type anywhere they want to ensure that the type of T is used.
Going back to our CircularQueue we want to push and pop methods so our queue is of some use.
struct CircularQueue<T> {
let bufferSize: Int
var buffer: Dictionary<Int, T>
var currentIndex: Int
var oldestIndex: Int
init(bufferSize: Int) {
self.bufferSize = bufferSize
buffer = Dictionary<Int, T>(minimumCapacity: bufferSize)
currentIndex = 0
oldestIndex = 0
}
mutating func push(element: T) {
currentIndex = (currentIndex+1)%bufferSize
buffer[currentIndex] = element
if currentIndex == oldestIndex {
oldestIndex = (oldestIndex+1)%bufferSize
}
}
mutating func pop() -> T? {
let item = buffer[oldestIndex]
buffer[oldestIndex] = nil
oldestIndex = (oldestIndex+1)%bufferSize
return item
}
}
As you can see we didn't have to use < >
after our method declaration to specify type. This is because T is already defined in our struct, so we can continue to use it as normal, ensuring our queue is only pushing and popping valid types.
Extensions
The last thing we will cover in this brief overview of generics are extensions. Extensions are similar to Objective-C categories, except they are a tad more powerful. Unlike their Objective-C brethren, extensions do not contain names.
In our example, we want to add the functionality to iterate over our queue using a for in
.
To do this we can make our queue conform to : Sequence
like so:
struct CircularQueue<T>: Sequence
but this kind of bloats the purity of our current implementation. The much better option, personally, is to organize our code through extensions.
extension CircularQueue : Sequence {
func generate() -> CircularQueueGenerator<T> {
var slice = Slice<T>()
for var index = 0; index < bufferSize; ++index {
if let value = buffer[(index + oldestIndex) % bufferSize]? {
slice.append(value)
}
}
return CircularQueueGenerator(items: slice)
}
}
There we go, now we are seperating our code into meaningful chunks, making it easier for other people to read our code.
If you look into the protocol Sequence
you will see it needs the function, generate
, which returns a generator object.
(For code completion I have included the Generator class below.)
Notice that like our functions, push and pop, we did not have to re-define T in our extension. Instead we are able to continue to use the T provided by our extended struct. This again allows all extensions to be type safe with whichever class they are extending.
If you want to get your hands dirty you can add an extension to our CircularQueue that adds printing functionality.
Objective-C Counterpart
Our very basic CircularQueue is a total of 65 lines, including whitespace. Now because we are all still new to Swift, we should compare how we would implement CircularQueue in Objective-C.
First we need to create an NSObject
subclass and call it . . . CircularQueue.
We can add private properties for a buffer and bufferSize and set them in a custom initializer. Add in push and pop methods that manipulate the buffer, pretty straight forward so far, but all of this is not safely typed. Any old id
can wander into our queue and destroy us.
Type Saftey
We could add type saftey to our queue by specifiying a Class
and introspecting every object that we push and compare its class type. This will only help a little as this would be a runtime comparison. Not to mention this can cause some headaches debugging how a foriegn object was inserted into our queue. Instead what we really want is a compile time check to make sure we are abiding by all the rules of the system we have defined.
Looping
Another issue we run into is looping. In swift we were able to extend our queue so that it can conviently loop over every element. If we wanted this in Objective-C we would have to expose an items property which returned an NSArray
.
This brings up another great feature of Swift. The ability to subscript our queue and call items directly, like so queue[0]
. We could do this one of two ways in Objective-C. We could expose a method, itemAtIndex:
or objectForKeyedSubscript:(id <NSCopying>)key
. The later requiring we pass an id
instead of an int
.
There are solutions for an Objective-C counter-part, but the I think the elegance of Swift starts to become a very appealing thing.
There are some pretty cool things generics can do and this article hasn't even begun to tap the power that generics can offer. For more advanced coverage of the power of generics I would turn you to WWDC '14 session 404. It is a great session covering advanced topics in Swift.
Generator struct for Sequence protocol
struct CircularQueueGenerator<T> : Generator {
var items: Slice<T>
mutating func next() -> T? {
if items.isEmpty {
return nil;
}
let item = items[0]
items = items[1..items.count]
return item
}
}