swiftmemoizationswift-structs

Swift memoizing/caching lazy variable in a struct


I drank the struct/value koolaid in Swift. And now I have an interesting problem I don't know how to solve. I have a struct which is a container, e.g.

struct Foo {
    var bars:[Bar]
}

As I make edits to this, I create copies so that I can keep an undo stack. So far so good. Just like the good tutorials showed. There are some derived attributes that I use with this guy though:

struct Foo {
    var bars:[Bar]

    var derivedValue:Int {
        ...
    }
}

In recent profiling, I noticed a) that the computation to compute derivedValue is kind of expensive/redundant b) not always necessary to compute in a variety of use cases.

In my classic OOP way, I would make this a memoizing/lazy variable. Basically, have it be nil until called upon, compute it once and store it, and return said result on future calls. Since I'm following a "make copies to edit" pattern, the invariant wouldn't be broken.

But I can't figure out how to apply this pattern if it is struct. I can do this:

struct Foo {
    var bars:[Bar]
    lazy var derivedValue:Int = self.computeDerivation()
}

which works, until the struct references that value itself, e.g.

struct Foo {
    var bars:[Bar]
    lazy var derivedValue:Int = self.computeDerivation()

    fun anotherDerivedComputation() {
        return self.derivedValue / 2
    }
}

At this point, the compiler complains because anotherDerivedComputation is causing a change to the receiver and therefore needs to be marked mutating. That just feels wrong to make an accessor be marked mutating. But for grins, I try it, but that creates a new raft of problems. Now anywhere where I have an expression like

XCTAssertEqaul(foo.anotherDerivedComputation(), 20)

the compiler complains because a parameter is implicitly a non mutating let value, not a var.

Is there a pattern I'm missing for having a struct with a deferred/lazy/cached member?


Solution

  • I generalized the problem to a simpler one: An x,y Point struct, that wants to lazily compute/cache the value for r(adius). I went with the ref wrapper around a block closure and came up with the following. I call it a "Once" block.

    import Foundation
    
    class Once<Input,Output> {
        let block:(Input)->Output
        private var cache:Output? = nil
    
        init(_ block:@escaping (Input)->Output) {
            self.block = block
        }
    
        func once(_ input:Input) -> Output {
            if self.cache == nil {
                self.cache = self.block(input)
            }
            return self.cache!
        }
    }
    
    struct Point {
        let x:Float
        let y:Float
        private let rOnce:Once<Point,Float> = Once {myself in myself.computeRadius()}
    
        init(x:Float, y:Float) {
            self.x = x
            self.y = y
        }
    
        var r:Float {
            return self.rOnce.once(self)
        }
    
        func computeRadius() -> Float {
            return sqrtf((self.x * self.x) + (self.y * self.y))
        }
    }
    
    let p = Point(x: 30, y: 40)
    
    print("p.r \(p.r)")
    

    I made the choice to have the OnceBlock take an input, because otherwise initializing it as a function that has a reference to self is a pain because self doesn't exist yet at initialization, so it was easier to just defer that linkage to the cache/call site (the var r:Float)