AssociatedType in Swift – A Generic Adventure

Hello ladies and gentlemen, Leo here. Today we’ll explore a great language feature called associatedtype in Swift.

In general terms, associatedtype is used when you need to have generic behavior 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 – AssociatedType in Swift

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

This problem screams for a generic solution. And that is exactly what we are going to do!

A brief explanation of associatedtype keyword

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

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 has type safety so any class that wants 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 both protocols is the function parameter.

Now the associatedtype comes in handy. You can use this feature to generalize the protocol type.

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

Now you can use 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 now has associated type requirements. If you try you will get:

generic constraint self requirements problem

 

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 as the Data in the worker protocol, I put the same name just to explain this. Each protocol has its 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 conformed 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, a 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 work with any type of data.

Check the implementation below:

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 the same type? You can catch why I did that observation. You must tell the Swift compiler that you will only accept Worker and Cache implementations and that the Data type inside each one is 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 want another full example of how this solution is generic, we can create a car struct and implement both protocols to the car class and…. Voilà! Check below:

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 works 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

And we are done!

 

Summary – AssociatedType in Swift

I hope you enjoy the reading as much as I enjoy learning and writing about *associatedtype*. Take it as food for thought and try to think about this as one more amazing tool in your day-to-day coding.

That’s all my people, I hope you liked reading this article as much as I enjoyed writing it. If you want to support this blog you can Buy Me a Coffee or leave a comment saying hello. You can also sponsor posts and I’m open to freelance writing! You can reach me on LinkedIn or Twitter and send me an e-mail through the contact page.

Thanks for the reading and… That’s all folks.

Credits – image

Share this post:

Related posts

Sponsor