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.
If the derived value is a generated property (as shown below), the correct value is always returned but is always recalculated.
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
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With