Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2015-02-08

Lazy and cache

Lazy properties are perfect if one needs to calculate a certain property only once. But it would be nice to have control over the lazy property in order to recalculate its value on demand.

I am probably not the only one who's first thought was to do the following:

class ClassWithLazyProperty {
    var a: Int {
        set { distance = nil }
        get { return self.a }
    }
    var b: Int {
        set { distance = nil }
        get { return self.b }
    }
    lazy var distance: Int? = {
        return self.b - self.a
    }()

}

While this compiles nicely, it does not work. After the first update to either 'a' or 'b' the distance will be 'nil' and the closure that calculates its value will never be called again.

So I needed something else. This was my second approach:

class ClassWithLazyProperty {
    var a: Int {
        set { p_distance = nil }
        get { return self.a }
    }
    var b: Int {
        set { p_distance = nil }
        get { return self.b }
    }
    var distance: Int {
        if p_distance == nil { p_distance = self.b - self.a }
        return p_distance!
    }
    private var p_distance: Int?
}

This actually works, but the code is horrible, and I have to do it again and again for each property that needs this kind of access.

Then I realised that what I needed was a cache. So here is my plea to Apple: Please introduce an attribute "cached" that would make the code in the first example work.

Absent the 'cached' attribute, we can still roll our own. Using generics and closures this looks actually pretty easy. My first approach was this:

class Cached<T> {
    
    private var cached: T?
    private var function: () -> T
    
    init(function: () -> T ) {
        self.function = function
    }
        
    func get() -> T {
        if cached == nil {
            cached = function()
        }
        return cached!
    }
    
    func reset() {
        cached = nil
    }
}

And when we need a cached variable:

class ClassWithCachedProperty {
    var a: Int { didSet { distance.reset() } }
    var b: Int { didSet { distance.reset() } }
    var distance = Cached<Int>(function: { [unowned self] in return self.b - self.a } )
    init() {
        a = 10
        b = 100
    }
    func doSomething() {
        println(distance.get())
    }
}

Unfortunately that does not compile. The compiler will insist that I use self before it is known. The solution would be to shove the initialisation of the Cached variable into the init function. But then the compiler complains that 'distance' is used before it is initialised.

Fortunately the second error message can be easily avoided:

class Cached<T> {
    
    private var cached: T?
    private var function: () -> T
    
    init(function: () -> T ) {
        self.function = function
    }
    
    func get() -> T {
        if cached == nil {
            cached = function()
        }
        return cached!
    }
    
    func reset() {
        cached = nil
    }
}

class ClassWithCachedProperty {
    var a: Int { didSet { distance.reset() } }
    var b: Int { didSet { distance.reset() } }
    var distance: Cached<Int>!
    init() {
        a = 10
        b = 100
        distance = Cached(function: { [unowned self] in return self.b - self.a } )
    }
    func doSomething() {
        println(distance.get())
    }
}

When we forget to put the proper initialisation in 'init' the thing will bomb on us during runtime. Not a perfect solution, but that possible bomb is easy to find during unit testing, so that is acceptable to me.

Happy coding

Edit: I have refined this approach in this post

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment