Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
like image 587
MrAlek Avatar asked Mar 06 '15 15:03

MrAlek


2 Answers

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.

like image 54
Jeremy David Giesbrecht Avatar answered Sep 19 '22 01:09

Jeremy David Giesbrecht


This is a really interesting question. I have a few different ideas here that could help.

First off, you are slightly misusing the idea of a lazy property. You can only have lazy stored properties because all that lazy does is delay execution until it is first executed. That value is then stored in the property from then on. You are dealing with a computed property which cannot be used in that way. You can certainly file a radar, but I think it is a lost cause because your use case is not a valid lazy case IMO.

With that said, I think you have a few options.

Option 1 - Use a Class with Property Observers

class Calculator {
    var value: Int {
        didSet {
            valueChanged = true
        }
    }

    var valueChanged = false

    var derivedValue: Int {
        if valueChanged {
            println("Doing expensive calculation")
            valueChanged = false
        }

        return self.value * 2
    }

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

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

The advantage here is that you can still lazily compute the derivedValue at the point that the property is called. The downside is that you are no longer using a "by value" object.

Option 2 - Compute Expensive Value in Mutate Method

struct SortOfLazyCalculator {
    var value: Int
    var expensiveComputedValue: Int = 0 // just guessing
    var derivedValue: Int {
        return self.value * 2
    }

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

    mutating func mutate() {
        value = random()
        expensiveComputedValue = random() // not sure what the expensive calculation is
    }
}

The advantage to this approach is that you can still keep your "by value" object, but you have to compute the expensive value at the time of mutation. You can't do it inside the derivedValue property because you cannot mutate self inside a computed property for a struct.

Option 3 - Use Static Struct to Monitor Value Changes

struct Struct {
    var value: Int
    var derivedValue: Int {
        struct Static { static var previousValue: Int? }

        if Static.previousValue == nil {
            println("Setting previous value since it is nil")
            Static.previousValue = value
        }

        if value != Static.previousValue! {
            println("Doing expensive calculation")
            Static.previousValue = value
        }

        return self.value * 2
    }

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

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

This approach allows you to keep your "by value" object while also allowing you to lazily compute the expensive value. The major issue here though is that this will only work for a single object. If you are creating multiple objects, this is a bad approach.

Summary

Unfortunately, this is not a valid use case for a lazy property. However, there are other approaches to solving this problem. Hopefully one of these will be sufficient. Based on all the info you have provided, I would venture a guess that Option 2 is probably your best bet.

like image 39
cnoon Avatar answered Sep 19 '22 01:09

cnoon