Type Erasure

Type Erasure is heavily used in Combine Framework in order to hide chain publishers, and before we dive in Combine, is convenient to get use to know Type Erasure.

Contents of this article:

  • Getting to know type erasure
  • Conclusions

Getting to Know Type Erasure

Have you ever seen yourself trapped with this error when working with generics in protocols?

Compiler complaining when not filling the type working with associated types in protocols

Lets reproduce this source of frustration for many us the first time we faced it with an example. We have the requirement to create an accounting system to keep track of the expenses and incomes. Swift allows us to create incredibly flexible systems thanks to protocols, so we are going to create one protocol, Accountable, to define countability system:

protocol Accountable {
    associatedtype TransactionType
    func count(transaction: TransactionType, id: String)
}

The associatedType is the way to create a generic type in protocols, in this case, TransactionType, which will be the transaction, expense or income, and as well, a method to register the transaction in the hypothetic system. Now, we are going to create a protocol to define what a transaction is:

protocol Transaction {
    var value: Float { get set }
    var date: Date { get set }
    var id: String { get set }
}

Easy, a transaction must consist in a value with type float, the date when the transaction happened, and an id. Done with the blueprints, lets create the system:

struct Income: Transaction {
    var value: Float
    var date: Date
    var id: String
}

struct Accounter: Accountable {
    func count(transaction: Transaction, id: String) {
        // Register income
    }
}

We have defined the struct Income that implements Transaction, and another struct named Accounting, that implements Accountable, that will take care of the registration of incomes.

let income = Income(value: 10.0, date: Date(), id: UUID().uuidString)
let accounter: Accountable = Accounter()
accounter.count(transaction: income, id: income.id)

Unfortunately, when defining accounter, we got the ominous error message:

Protocol ‘Accountable’ can only be used as a generic constraint because it has Self or associated type requirements

What does means this message? An associated type is a placeholder for a type in a protocol, and we have to fill the placeholder, with a type, just using an instance of type that implements the protocol is not enough information for the compiler in order to work. How can we solve this? The message is already giving us a hint, which is one of the most used ways of type erasures, working with a generic wrapper.

class AnyAccounter<TransactionType>: Accountable {
    private let counTransaction: (TransactionType, String) -> Void

    init<Account: Accountable>(anyAccounter: Account) where Account.TransactionType == TransactionType {
        self.counTransaction = anyAccounter.count
    }

    func count(transaction: TransactionType, id: String) {
        counTransaction(transaction, id)
    }
}

Uh oh! Thats quite a daunting pice of code if you are not use to work with generics. Why do we need this, can we just move forward using a concrete type? Yeah, well, we could, but then we loose all the power that generics protocols can provide us to create generic apis, and type erasure enable use to work with generic protocols that have requirements that are specific to a type. Lets decipher AnyAccounter class:

AnyAccounter class has a generic constraint of type TransactionType, implements Accountable protocol, and its initializer is constrained to a generic type Account that has to implement the Accountable protocol, and as well, it accepts the parameter of generics type Account, making sure that the generic type associated type is of the same type. Take your time, I had to take as well in order to wrap around my head this idea, and still it is a little bit confusing.

Summarizing, AnyAccounter works as a box that wraps around the type (hiding or erasing it) that will work with the Accountable protocol, giving a reference to the compiler so the type safety is not broken. Lets finish the example, it will be clearer this way:

let income = Income(value: 10.0, date: Date(), id: UUID().uuidString)
let incomeCounter = Accounter()
let accounting = AnyAccounter<Transaction>(anyAccounter: incomeCounting)
accounting.count(transaction: income, id: income.id)

So, have created an income transaction, and using the previously defined Accounter, have instantiated it, AND, instantiated as well AnyAccounter, providing the Associated type, which in this case in Transaction, and passing the instance of Accounter, incomeCounter to AnyAccounter. This type we are not getting the annoying error message, and can register our income earned with sweat, trying to understand Type Erasure.

Conclusion

The above example only copes one of the cases, but there are many others to deal with type erasure, and I invite you to keep investigating it, as it is heavily used in Combine Framework, which will be introduced in the next article!

Comments, typos, errors?

Please, if you see any error, typo, way to improve it, or just one to say Hi, don’t hesitate to contact!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: