The other day I was writing some code and went a bit too generic happy with all my classes. A simplified version started with some code like this:
This is a pretty simple pattern. You have a type which must delegate responsibilty to some other type. This simple and contrived example gets the point across. A more practical example would be a UITableViewCell with a button on it. When the button is clicked, the cell is in no position to make any decision or to do any work for the user, so it delegates the action backwards towards it’s delegate which is some UIViewController subclass.
From here, I wanted to be more “Swifty” and make my code generic for maximum reusability. I wanted NumberHolder to work for Doubles, too(or any other type, in fact.)
(Note: this code below doesn’t work.)
The delegate clearly doesn’t know what type of object to expect. It has to be told that it will receive a generic type.
However, it doesn’t know what a “Number” is. This is where the famous associatedtype comes in.
Great! The delegate makes sense. But now we have the famous error that confuses so many about protocols with associated types.
What does this mean? Well, the problem is that the NumberHolder type referes to a variable of the type NumberHolderDelegate. When the compiler goes to check that property, it requires to know what the associatedtype “Number” is. However, simply declaring “NumberHolderDelegate” doesn’t tell the compiler this. “associatedtype Number” is a statement of “to be determined later” or “to be determined by some other means.” We have to tell the compiler what that type is going to be. How? One way is through generics. The NumberHolder class turns into this:
(Note: this still doesn’t work quite yet.)
We make NumberHolder generic over the type Delegate and tell the compiler that Delegate is a NumberHolderDelegate. Almost working. However, that was only half the fix. The compiler still doens’t know what the associatedtype “Number” is. Here’s how we tell it this information:
The added clause “Delegate.Number == Number” tells the compiler that the type Delegate is (first of all a NumberHolderDelegate, but it already knew that from the first clause) declared with an associatedtype of the same type that the NumberHolder is. So upon creation of a NumberHolder:
(Note: this STILL isn’t correct as the second generic type isn’t being declared.)
the compiler can (almost) infer what is going on. First, we tell it that the instance is NumberHolder<Double,(Placeholder)>. From the first where clause, the compiler knows that Delegate is a NumberHolderDelegate. From the second clause, the compiler knows that the Delegate’s associatedtype Number is going to be == to the NumberHolders generic type Number. In this case, that means the associatedtype is a Double.
However, the compiler hasn’t been told exactly what object is going to take the place of the second generic type. In typical practices, you simply do someting like this:
This creates a type that can serve as the delegate to the NumberHolder specific to the type Int. We can also make this generic as well:
Now this class will work with any NumberHolder regardless of type. Now about this time, I realized I accidentally stumbled upon type-erasure. We just created a class (SomeOtherClass) that lets us hide away the NumberHolderDelegate from the NumberHolder and just accept any SomeOtherClass. In fact, this is where the naming “AnySuchAndSuch” comes from. We’ll convert to this convention:
This enables us to remove the second generic type from NumberHolder and all it’s where clauses:
This is a MUCH nicer class declaration. We traded an intermediary class, the AnyNumberHolderDelegate, for one generic type and two where clauses.
One last caveat: this isn’t EXACTLY type erasure. Typically type erasure merely copies the delegate function (or receives a closure in it’s initializer) that serves as the delegate function. A truly type erased delegate would be as such:
And this is our type-erased class to serve as the delegate to our NumberHolder class. Alltogether we have: