MrAlek MrAlek - 21 days ago 5
Swift Question

How to properly make a lazy derived property on a mutating struct in Swift?

I'm making a mutating struct with a really expensive-to-compute derived value. So what I want to do is to compute this derived value lazily and store the result, until the struct gets mutated again, at which point the derived value is no longer valid and needs to be recomputed.

(Failed) Option 1: Generated property



If the derived value is a generated property (as shown below), the correct value is always returned but is always recalculated.

(Failed) Option 2: Lazy-loaded property



If it is a lazy property instead, the calculation is only done once... ever. So once the struct is mutated, the derived value is wrong and won't be recomputed. Also, I can't access the property if I assign a constant value from the struct.

Is there any possible solution in Swift 1.2 or do I need to file a radar?

struct Struct {
var value: Int

// Option 1: Generated property
var derivedValue: Int {
println("Doing expensive calculation")
return self.value * 2
}

// Option 2: Lazy property
lazy var derivedValue: Int = {
println("Doing expensive calculation")
return self.value * 2
}()

init(value: Int) {
self.value = value
}

mutating func mutate() {
value = random()
}
}

var test = Struct(value: 2)
test.derivedValue
test.derivedValue // If not lazy, expensive calculation is done again here
test.mutate()
test.derivedValue // If lazy, this has wrong value

let test2 = test
test2.derivedValue // Compiler error if using lazy implementation

Answer

Using an embedded class gets around the limitations on mutating a struct. This lets you use a by‐value type that does not run expensive computations until they are needed, but still remembers the result afterward.

The example Number struct below computes and remembers its square property in a way that behaves just like you describe. The math itself is ridiculously inefficient, but it is a simple way to illustrate the solution.

struct Number {

    // Store a cache in a nested class.
    // The struct only contains a reference to the class, not the class itself,
    // so the struct cannot prevent the class from mutating.
    private class Cache {
        var square: Int?
        var multiples: [Int: Int] = [:]
    }
    private var cache = Cache()

    // Empty the cache whenever the struct mutates.
    var value: Int {
        willSet {
            cache = Cache()
        }
    }

    // Prevent Swift from generating an unwanted default initializer.
    // (i.e. init(cache: Number.Cache, value: Int))
    init(value: Int) {
        self.value = value
    }

    var square: Int {
        // If the computed variable has been cached...
        if let result = cache.square {

            // ...return it.
            print("I’m glad I don’t have to do that again.")
            return result
        } else {

            // Otherwise perform the expensive calculation...
            print("This is taking forever!")
            var result = 0
            for var i = 1; i <= value; ++i {
                result += value
            }

            // ...store the result to the cache...
            cache.square = result

            // ...and return it.
                return result
        }
    }

    // A more complex example that caches the varying results
    // of performing an expensive operation on an input parameter.
    func multiple(coefficient: Int) -> Int {
        if let result = cache.multiples[coefficient] {
            return result
        } else {

            var result = 0
            for var i = 1; i <= coefficient; ++i {
                result += value
            }

            cache.multiples[coefficient] = result
                return result
        }
    }
}

And this is how it performs:

// The expensive calculation only happens once...
var number = Number(value: 1000)
let a = number.square // “This is taking forever!”
let b = number.square // “I’m glad I don’t have to do that again.”
let c = number.square // “I’m glad I don’t have to do that again.”

// Unless there has been a mutation since last time.
number.value = 10000
let d = number.square // “This is taking forever!”
let e = number.square // “I’m glad I don’t have to do that again.”

// The cache even persists across copies...
var anotherNumber = number
let f = anotherNumber.square // “I’m glad I don’t have to do that again.”

// ... until they mutate.
anotherNumber.value = 100
let g = anotherNumber.square // “This is taking forever!”

As a more realistic example, I have used this technique on date structs to make sure the non‐trivial computations for converting between calendar systems are run as little as possible.

Update

Some of the boilerplate code in the answer above can be reduced by using a small module I wrote for just this purpose (or by copying its 12 lines of source code). Its project description contains a usage example based directly on the example given above.