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:
CGFloat
. It just seems like a weird API designrotation()
could be called multiple times, which would result in us paying the calculation cost more than onceSo 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:
rotation
value isn’t used, then it’s never calculated, but we didn’t have to add much additional complexity to our api to achieve this.CGFloat
, as the naming of Lazy
is pretty self-explanatory.Lazy
is also a class, so it has reference semantics. If anybody calls rotation.value
, then everybody will subsequently receive the cached result after that.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.
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!