Steve On Stuff

Lazy Arguments

28 Apr 2018

How can you call a delegate or closure callback, passing in arguments that are lazily calculated? This was an issue that I had recently, so I thought I’d share how I solved it.

So why would you want to do this? I came across this requirement when working on TweenKit. One of TweenKit’s features is the ability to animate an object over a bezier path. It’s up to the caller to actually update the object’s position and rotation, though, so TweenKit simply calls a closure each frame with updated position and rotation values:

let action = BezierAction(path: bezierPath, duration: 1.0, update: { (position, rotation) in
            // Update object position and rotation
        })

Often though, the caller doesn’t need the rotation value, and they’ll just animate the object’s position. The issue with this is that rotation is pretty expensive to calculate, so we don’t want to waste a bunch of cpu cycles figuring it out if it’s then just discarded.

I could solve this by instead passing in a closure that calculates the rotation:

 (position: CGFloat, rotation: () -> CGFloat) -> ()

Although there’s a few issues with this:

The Lazy Type

So here’s my solution, the Lazy type:

public class Lazy<T> {
    
    private let calculateValue: () -> (T)
    private var cachedValue: T?
    
    public init(calculate: @escaping () -> (T)) {
        self.calculateValue = calculate
    }
    
    public var value : T {
        get {
            if cachedValue == nil {
                cachedValue = calculateValue()
            }
            return cachedValue!
        }
    }
}

Lazy<T> takes a closure that calculates a value of type T. The first time that the value property is accessed, the result will be calculated using the closure and cached. Any subsequent calls to value will return the cached value.

Here it is in action:

let lazyDouble = Lazy<Double> {
    print("Perform Complex Calculation")
    return 2 + 8
}

print("Start Loop")
(0..<5).forEach { _ in
    print(lazyDouble.value)
}
Start Loop
Perform Complex Calculation
10.0
10.0
10.0
10.0
10.0

Now we can rewrite the update closure to look like this:

(position: CGPoint, rotation: Lazy<CGFloat>) -> ()

Here’s what we’ve bought ourselves:

A note on capture lists

In this api, it’s not really intended for the caller to actually store the position and rotation values for later, they’re really intended to be used within the update closure (they’d be stale anyway). Nothing prohibits this though, and there’s a gotcha here. If you create your lazy value like this:

var values = [5.0, 10.0, 15.0]

let lazyAverage = Lazy<Double> {
    values.reduce(0, +) / Double(values.count)
}

values is not copied, so if it’s mutated before lazyAverage.value is called, then lazyAverage will return the average of the current state of values, not the state that it was it when lazyAverage was created.

var values = [5.0, 10.0, 15.0]

let lazyAverage = Lazy<Double> { [values] in // <-- Copy values
    values.reduce(0, +) / Double(values.count)
}

Capturing values in the closure’s capture list copies it, ensuring that even if you store it and call it later, it will still return the average of values at the time that lazyAverage was created.

Copying has it’s own cost though, so it might not the right solution in all cases, especially if the values are large.

Refactoring

I recently rewrote the lazy type to look like this:

public class Lazy<T> {
    
    private indirect enum State<T> {
        case closure( () -> (T) )
        case value(T)
    }
    
    private var state: State<T>
    
    public init(calculate: @escaping () -> (T)) {
        self.state = .closure(calculate)
    }
    
    public var value : T {
        get {
            switch state {
            case .value(let value):
                return value
            case .closure(let closure):
                let result = closure()
                self.state = .value(result)
                return result
            }
        }
    }
}

It’s a bit harder to read, but does have a subtle benefit. Lazy.State can hold either a closure that creates T, or a value of T. When the closure is run, state is set to .value(T) which deallocates the closure. Depending on how large the objects are that are captured by the closure, this should reduce the memory footprint. In all likelihood, this isn’t an optimisation that would make much difference to most applications, but it doesn’t hurt either!

As is often the case in Swift, a custom type can usually solve a problem!

Next »


« Previous




I'm Steve Barnegren, an iOS developer based in London. If you want to get in touch: