Coding practices at their worst. A rant about retain loops that appear when instance methods are used as callbacks.
Hello there, my sweet fellow developers,
Recently one of LA-based startups I am acquainted with approached me to ask for the code assessment. They had terrible problems with code instability, random crashes and stuff like that. What can I say? The code was rich with mistakes I’ve mentioned in my previous article (especially force unwraps and force casts, to which I salute, as they were the reason behind loads of crashes). But there was one particular mistake that I’ve been seeing over and over in different projects since people understood that all the functions in Swift are first class higher order.
Just a quick reminder, first class higher order functions are the functions that you can put in a variable, send as a parameter to other function and receive as a result to some function execution.
So, lets take a look at a general simplified case:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class Processor { typealias Callback = (Processor) -> () var callback: (Callback)? func process() { // ... do something self.callback?(self) } } class Data { let processor = Processor() init() { self.processor.callback = self.onProcessingFinished } func process() { self.processor.process() } func onProcessingFinished(processor: Processor) { // ... do something } } |
Disclaimer: This is a sample code outlining the approach, same applies to other snippets. Pardon me, my readers, who do understand that. You would be shocked at how many people do not get that all my articles describe the general approach, and not specific use cases.
Processor is some entity that performs processing and calls the callback after the processing is finished. Data in that case, is some entity that owns processor and is interested in the callback that is called when the processing is finished. One of the simplest user stories is when Data is UIViewController subclass and Processor is UIView subclass, which calls callback from controller when the user interacts with the view in some way.
So, what’s wrong with this code? We have a retain loop in here. Neither Data, nor Processor will ever get deallocated. It’s pretty trivial to check that my statement is indeed true. Just put the debug print in deinit methods of both Processor and Data. As you can see, it never prints anything, as deinit was never called. And that’s because of the retain loop.
But why the retain loop actually appears in that case? In order to grasp that you need to understand that any struct and class instance function in swift is a closure. Closure is a type of function that captures and stores the external context (e.g. variables). A perfect closure example is:
1 2 3 4 5 6 7 8 9 10 11 |
func autoincrementedIDGenerator() -> () -> Int { var id = 0 return { let result = id id += 1 return result } } |
The autoincrementedIDGenerator returns a function that captures and operates with id. Each time we call the function it creates a new variable named id and returns a function that captures that variable.
1 2 3 4 5 6 7 |
func testGenerator(_ name: String, generator: () -> Int) { (0...2).forEach { _ in print("\(name) = \(generator())") } } testGenerator("user id", generator: autoincrementedIDGenerator()) testGenerator("image id", generator: autoincrementedIDGenerator()) |
When we test the output we’d receive:
1 2 3 4 5 6 7 |
user id = 0 user id = 1 user id = 2 image id = 0 image id = 1 image id = 2 |
It is in line with what we expect. Each of the function calls generated by generator returns a separate function capturing its own id.
So, as a step forward, lets take a look at the type of instance method of Data class (same applies to structs, enums, etc.):
1 2 3 4 5 6 |
let f = Data.onProcessingFinished print(type(of: f)) // prints (Data) -> (Processor) -> () |
Instance signature is obviously (Processor) -> (), so where did the first Data parameter appear? That’s because of how Swift instance methods work: to generate instance methods from type methods the type method receives the instance and returns a closure, that captures that instance. So, virtually, function calls below are all equivalent:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let data = Data() data.process() let instanceProcess = data.process instanceProcess() let typeProcess = Data.process let instanceFromTypeProcess = typeProcess(data) instanceFromTypeProcess() Data.process(data)() |
The quirk in here is that type method, which generates instance method, captures the instance itself strongly for reference types. So, while the instance method lays in some strong variable (Processor.callback) the instance it’s related to won’t be deallocated as well. A quick test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class Function { deinit { print("function deinit") } func f() { } } func async(_ f: @escaping () -> ()) { DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.1, execute: f) } async { let function = Function() let f = function.f print("function created") async { f() print("function used") async { print("last async") } } } // prints function created function used function deinit last async |
So, the important question is how do we deal with that problem for the original case of Data and Processor?
The simplest solution would be to fix the Data.init:
1 2 3 4 5 6 |
init() { self.processor.callback = { [weak self] processor in self?.onProcessingFinished(processor: processor) } } |
However, that’s just too much boilerplate code. This solution also has some duplication that obviously sucks. And just imagine such senseless monsters that obscure the idea by imposing a lot of syntax just to satisfy the language all around the code. So, lets improve the solution and generalize it by using the might of Swift generics and type inference:
1 2 3 4 5 6 7 8 |
func weakifyFunction<Entity, Parameters>(_ f: @escaping (Entity) -> (Parameters) -> (), object: Entity) -> (Parameters) -> () where Entity: AnyObject { return { [weak object] parameters in object.map { f($0)(parameters) } } } |
In case you wonder, why I used map, take a look at my previous article Type Inference in Swift.
Everything should be clear based on the previous explanations except for one thing. I think you wonder as well what the hell is Parameters. You see, under the hood type methods expect the second argument to be a tuple:
1 2 3 4 5 6 7 8 9 |
class Function2 { func f(x: Int, y: Int) { } } print(type(of: Function2.f)) // prints (Function2) -> ((Int, Int)) -> () |
Note, that ((Int, Int)) is in double braces, which means, that it’s a tuple serving as a function argument. So, Parameters is just a generic type for a tuple that would contain instance method parameters. And the number of parameters could be anything from empty tuple to tuples with about 10 parameters or more (functions with 10 parameters mean, that you have problems in your decomposition though).
Data.init would look much better after we use weakifyFunction as well.
1 2 3 4 |
init() { self.processor.callback = weakifyFunction(Data.onProcessingFinished, object: self) } |
Much better, huh? And as a bonus to retain loop elimination we receive reusability and intent clarity.
That’s all, folks. Have a great day and stay DRY no matter where you are.