Swift lazy var on immutable structs

80 Views Asked by At

I'm working with structs that stores data and makes some cached operations on it e.g.:

struct Number {
  let value: Int
  lazy var square = pow(Double(value), 2)
  lazy var squareRoot = Double(value).squareRoot()
  lazy var factorial = (1...value).reduce(1, *)
}

It works OK unless the it's variable:

var number = Number(value: 9)
number.square // 81
number.squareRoot // 3
number.factorial // 362 880

Otherwise you get the following error if it's constant:

let number = Number(value: 9)
number.square // Cannot use mutating getter on immutable value: 'number' is a 'let' constant

There are several popular solutions:

1. Change 'let' to 'var' to make it mutable

It works with local variables only and doesn't with func parameters because they are constants by default and it forces to make a copy inside:

func f(number: Number) {
  var n = number
  n.factorial
}

2. Convert struct to class

It's not an option in my case.

3. Use additional properties to store cached results

According to these solutions https://stackoverflow.com/a/32292456/979986 and https://oleb.net/blog/2015/12/lazy-properties-in-structs-swift/ we can create a dedicated class instance to cache all our calculations:

For instance:

struct Number {
  let value: Int
  
  class Cache {
    var square: Double?
    var squareRoot: Double?
    var factorial: Int?
  }
  private let cache = Cache()
  ...
  var factorial: Int {
    guard let factorial = cache.factorial else {
      let res = (1...value).reduce(1, *)
      cache.factorial = res
      return res
    }
    return factorial
  }
}

let number = Number(value: 9)
number.factorial // 362 880

But this solution is redundant and inconvenient because I have to clone all my properties in all my structs.

Question: Is there any other convenient approaches for most cases?

2

There are 2 best solutions below

6
iUrii On

It needs to store cached values outside your struct to make a common solution for immutable instances. Then you can access to this storage from your computed properties by a key than can be a location of code where this call appears:

class Lazy {
  private static var cache = [Int : Any]()
  
  static func `var`<T>(file: String = #file, line: Int = #line, column: Int = #column, f: () -> T) -> T {
    objc_sync_enter(self)
    defer { objc_sync_exit(self) }
    
    let key = "\(file):\(line):\(column)".hashValue
    guard let value = cache[key] as? T else {
      let value = f()
      cache[key] = value
      return value
    }
    return value
  }
}

Not it works like lazy properties on constant instance:

struct Number {
  let value: Int
  var square: Double { Lazy.var { pow(Double(value), 2) } }
  var squareRoot: Double { Lazy.var { Double(value).squareRoot() } }
  var factorial: Int { Lazy.var { (1...value).reduce(1, *) }}
}


let number = Number(value: 9)
number.square // 81
number.squareRoot // 3
number.factorial // 362 880
3
Joakim Danielson On

You can create a private and a public property for each "lazy" property and make the private one optional to know when to calculate a value or return the stored one

Example with square

struct Number {
    private var _square: Double?

    let value: Int
    var square: Double {
        get { _square ?? pow(Double(value), 2) }
        set { _square = newValue }
    }

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