Search code examples
swiftprotocolslazy-loading

Swift protocol with lazy property - Cannot use mutating getter on immutable value: '$0' is immutable


Goal: Create a protocol that allows lazy computation of a property for structs that conform to the protocol and then add the properties for an array of those structs. The computation is intensive and should only be executed once, hence the lazy requirement.

So, after lots of reading (eg this: Swift Struct with Lazy, private property conforming to Protocol) and trial and error (please don't flag this as a duplicate, unless it actually addresses this exact case), I came up with something that works:

import Foundation

protocol Foo {
    var footype: Double { mutating get }

    func calculateFoo() -> Double
}

struct Bar: Foo {
    private lazy var _footype: Double = {
        let value = calculateFoo()

        return value
    }()

    var footype: Double {
        mutating get {
            return _footype
        }
    }

    func calculateFoo() -> Double {
        print("calc")
        return 3.453
    }
}

Testing this in a Playground:

var bar = Bar()
print(bar.footype)
print(bar.footype)

And the output is:

calc
3.453
3.453

So far, so good.

Now, I want to make an array of Bar and add the footype properties:

var bar1 = Bar()
var bar2 = Bar()
var bar3 = Bar()
var bars = [bar1, bar2, bar3]

print(bars.map { $0.footype }.reduce(0.0, +))

Which gives me the following error:

Cannot use mutating getter on immutable value: '$0' is immutable

Found a lot of info on this error, but am stuck how to solve it. One way would be to use class instead of struct, but that doesn't work well with other parts of the code.

Is it possible what I am trying to achieve?


Solution

  • As you can tell from the error message, $0 is immutable, so you can't use a mutating member on it.

    Therefore, you can't iterate over the bars directly. What you can do is iterating through its indices, because we know that bars[$0] is mutable:

    print(bars.indices.map { bars[$0].footype }.reduce(0.0, +))