
Ahh Swift protocols and generics. When I first started learning about them, my organizational impulses went wild. All the possibilities for meticulously crafted hierarchies ... it was an OCD dreamworld.
That is, until I ran into this:
Protocol can only be used as a generic constraint because it has Self or associated type requirements
With one error, my dreams came crashing to a halt. I could not have collections of protocols with associated types!
Initially in disbelief, I went down every possible rabbit hole (sometimes more than once) to work around this problem, often with none of the solutions out there really meeting my needs.
Here's what I found, and what I eventually decided to do instead.
The core problem
Once upon a time I was experimenting with a personal finance app, and I had a protocol that modeled a debt:
Super. Works great so far.
The problem arose when I tried to add interest. You see, different kinds of debts calculate interest differently, so I thought it would be cute to encapsulate this logic into it's own protocol and set of concrete implementations:
But in order to do that properly, I need associated types:
Because doing so allows me to be type specific in my specific debt implementation:
And boy was I indignant. I thought for sure this couldn't be the case. There MUST be a way!
Basic type-erasure
After pulling my hair out with my own failed workarounds, I turned to the Google-sphere and discovered what appeared to be a miracle panacea: a magical pattern called "type-erasure."
Type-erasure simply means "erasing" a specific type to a more abstract type in order to do something with the abstract type (like having an array of that abstract type). And this happens in Swift all the time, pretty much whenever you see the word "Any."
So I followed the first type-erasure tutorial I could find, and ended up with this:
Elaborate type-erasure
Not so fast.
While our simple basic type-erasure solution makes it possible to have an array of debts, we're super limited as to what we can do with it. Unlike our mixed array of strings and integers, we can't do something like this:
If we try, we get this warning:
Cast from 'AnyDebt' to unrelated type 'CreditCardDebt' always fails
And there's about a million different flavors that each have their trade-offs, and tutorials all over the internet that share their attempts at each. I actually once tried to reverse engineer Apple's approach but ended up with some really awkward usage patterns that I just scrapped.
All in all, type-erasure is not the panacea I thought it would be — not even close.
Architecting around type-erasure
Since type-erasure is actually a huge pain — pretty often — I eventually let go of my indignation and started to think about solving my problem from a fundamentally different perspective. I asked myself whether I really needed type-erasure at all.
Turns out, if you rethink the way you architect your classes, there's often better ways around this problem.
But if we do that, how can we have different types of interest objects to encapsulate the various kinds of calculations while maintaining access to their type information?
One way is with enums.
Sure, it feels a little redundant to have an enum type and a concrete interest type, and I can't helped but be irked by it, but it's a much lower hassle work-around than dealing with the complexities of type-erasure.
More often than not, there's probably some way you can redesign your protocols and structs to avoid type erasure altogether.
About Triplebyte
Triplebyte helps engineers find great jobs by assessing their abilities, not by relying on the prestige of their resume credentials. Take our 30 minute multiple-choice coding quiz to connect with your next big opportunity and join our community of 200,000+ engineers.