AssociatedTypes in Swift - A Generic Adventure

Subscribe to my newsletter and never miss my upcoming articles

Hello ladies and gentlemen, Leo here.

Today we'll discuss a great language feature called associatedtype. In general terms, associatedtype is used when you need to have generic behaviour in your protocols. It's important because as apple says:

Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner.

So let's move to the problem and the actual solution.

The Problem

You want your controller to work with various types but it will have always the same behaviour, first it'll execute some data processing and after that you want to save in the specific data cache.

A brief explanation of associatedtype

If you try to resolve the problem above with just protocols, you will have a bunch of protocols like this:

protocol CarWorker {
    func process(car : Car)
}

protocol UserWorker {
    func process(car : User)
}

// and you will have the types conforming 

struct CarController : CarWorker {
    func process(car: Car) {
        //compute car data
    }
}

struct UserController : UserWorker {
    func process(car: User) {
        // compute user data
    }
}

And as you know, Swift have type safety so any class that want to use both protocols must call exactly that type. Example:

class HipoteticalController {
    var userWorker : UserWorker!
    var carWorker : CarWorker!

    init(userWorker: UserWorker, carWorker : CarWorker) {
        self.userWorker = userWorker
        self.carWorker = carWorker
    }
} // or something like this

But you can notice that the only difference between the both protocols is the func parameter. Now the associated type come in handy. You can use this feature to generalize the protocol type.

protocol Processor {
    associatedtype Data
    func process(data: Data)
}

Now you can use like the same protocol for everything!

struct CarController : Processor {
    func process(data: Car) {
        //compute car data
    }
}

struct UserController : Processor {
    func process(data: User) {
        // compute user data
    }
}

Very cool, isn't it? And everything that uses this kind of behavior in your code base, will now only have to deal with one type, the Processor.

class HipoteticalController<P: Processor> {
    var processor : P!

    init(processor: P) {
        self.processor = processor
    }
}

You can notice that now we use associatedtype in the protocol we can no longer pass it directly to the init method. You have to pass it as a generic constraint because it's now have associated type requirements. If you try you will get:

Screen Shot 2020-12-05 at 10.47.23.png

Solving the problem

After this brief introduction, we can attack the problem stated above. First we will make the base User struct and the two protocols.

struct User {
    var name: String
}

protocol Worker {
    associatedtype Data
    var finished : [Data]? {get set}
    func process(data: Data, completion: @escaping (Data?)->Void )
}

protocol Cache {
    associatedtype Data
    func save(data: Data)
    func retrieve(_ id: Int)-> Data?
    func retrieveAll() -> [Data]?
}

Note that the Data in the Cache protocol isn't the same type the Data in the worker protocol, I put the same name just to explain this. Each protocol has it's own associeatedtype and the same name doesn't guarantee it's the same Type. Remember that.

Second, we conform to both protocols for User use.

class UserWorker : Worker {

    var finished: [User]?

    func process(data: User, completion: @escaping (User?)->Void ) {

        if data.name.count > 5 { // simulation of some processing
            completion(nil)
        } else {
            if var finished = finished {
                finished.append(data)
            } else {
                finished = [User]()
                finished?.append(data)
            }
            completion(data)
        }
    }
}

struct UserCache: Cache {

    func save(data: User) {
        print("saving user logic")
    }

    func retrieve(_ id: Int) -> User? {
        // some retrieve cache logic, just placeholder code here
        return User(name: "Ana")
    }

    func retrieveAll() -> [User]? {
        return [User(name: "Leo")]
    }
}

Now we have both conforming to the protocols, we can create a generic class that will receive the Worker and the Cache, and now we answered the question. This is what we plan all the time, some class receiving generic protocols and working with various types at the same time we have type safety! WorkerController is a great example of the Swift type safety because it can use various workers and various caches that works with any type of data.

class WorkerController<W: Worker, C: Cache> where W.Data == C.Data {

    var worker: W
    var cache: C

    init(worker: W, cache: C) {
        self.worker = worker
        self.cache = cache
    }

    func execute(data: W.Data) {
        worker.process(data: data) { dataProcessed in
            if let data = dataProcessed {
                self.cache.save(data: data )
            } else {
                print("Couldn't proccess data")
            }
        }
    }
}

It's very important to notice the "where W.Data == C.Data". Remember I told you that the same associatedtype name not necessarily will be same type? You can catch why I did that observation. You must tell to the Swift compiler that you will only accept Worker and Cache implementations that the Data type inside each one are the same.

And finally you can test:

let userWorkerController = WorkerController(worker: UserWorker(), cache: UserCache())

userWorkerController.execute(data: User(name: "John")) // will process
userWorkerController.execute(data: User(name: "Seraphin")) // won't process

And if you and another full example of how this solution is generic, we can create a car struct and implement both protocols to car and.... Voilà!


struct Car {
    var brand: String
    var price: Double
}

class CarWorker : Worker {

    var finished: [Car]?

    func process(data: Car, completion: @escaping (Car?)->Void ) {

        if data.brand.count > 5 || data.price < 10000.00 { // just simulating some king of processing
            completion(nil)
        } else {
            if var finished = finished {
                finished.append(data)
            } else {
                finished = [Car]()
                finished?.append(data)
            }
            completion(data)
        }
    }
}

struct CarCache: Cache {

    func retrieveAll() -> [Car]? {
        return [Car(brand: "Ferrori", price: 1010100)]
    }


    func save(data: Car) {
        print("saving Car logic")
    }

    func retrieve(_ id: Int) -> Car? {
        // some retrieve cache logic, just placeholder code here
        return Car(brand: "Ferrori", price: 1010100)
    }
}

The WorkerController still work without moving a single line of code.

let carWorkerController = WorkerController(worker: CarWorker(), cache: CarCache())

carWorkerController.execute(data: Car(brand: "Fard", price: 15000)) // will process
carWorkerController.execute(data: Car(brand: "Ferrori", price: 2000)) // won't process

I hope you enjoy the reading as much I enjoy learning and writing about associatedtype. Any thoughts and comments please share bellow.

Thanks for the reading and... That's all folks.

Image: Antonio Zucchi

No Comments Yet