Should `Observable` be factored out of "Containers" down to smaller Storage types?

I spent some time hacking around a runtime crash from Observable and think I have a legit workaround for now. It actually leads to a different question about observability and what "scope" should Observable be applied to.

Here is my original code:

@Observable final class Repeater<each Parameter, Output> {
  let parameter: (repeat each Parameter)
  var output: Output
  
  init(
    parameter: (repeat each Parameter),
    output: Output
  ) {
    self.parameter = parameter
    self.output = output
  }
}

let repeater = Repeater(parameter: (1, 2, 3), output: 4)
repeater.output = 5

This code (as of 6.0 beta toolchain) crashes at runtime because KeyPath access (which is needed for Observable) is not supported across a variadic type.

Here is my workaround:

final class Repeater<each Parameter, Output> {
  let parameter: (repeat each Parameter)
  let storage: Storage<Output>
  var output: Output {
    get {
      self.storage.output
    }
    set {
      self.storage.output = newValue
    }
  }
  
  init(
    parameter: (repeat each Parameter),
    output: Output
  ) {
    self.parameter = parameter
    self.storage = Storage(output: output)
  }
}

@Observable final class Storage<Output> {
  var output: Output
  
  init(
    output: Output
  ) {
    self.output = output
  }
}

let repeater = Repeater(parameter: (1, 2, 3), output: 4)
repeater.output = 5

Since my Observable type is no longer a variadic type, this code runs with no runtime crash.

This got me thinking… if I have any arbitrary Observable type with one stored property that needs to be observed that may not need to be generic (or the generic constraint is only a subset of the generic constraints on the "parent" container type)… is it normally considered a "best-practice" to factor that Observable type out to a storage helper that is as simple as possible (and remove the Observable from the Parent Container)? Are there any compile time (or runtime) optimizations that would be unlocked with this approach)?

I can't think of any reasons this refactoring would lead to problems… but are there any legit benefits (other than working around this specific runtime crash) other than just an arbitrary "style" and personal engineering preference for organizing code?

2 Likes

Just curious, does your approach works with Bindable? (I guess not)

I think the official approach to achieve a similar purpose is ObservationIgnored macro, but that may crash in your case too.

The crash is not really directly tied to Observable as much as it is tied to KeyPath (and Observable currently depends on KeyPath). The implication is that a variadic type may still have (potential) effects on the performance of Observable even if the variadic properties themselves are ObservationIgnored. The question then (in a general sense) is what effects might any generic type have on the performance of Observable when the ObservationTracked property is not generic (or only needs to be generic over one of the generic constraints on the Observable class).