Swift-CowBox: Easy Copy-on-Write Semantics for Structs

mutating func can access many stored properties. With current macro imp. access to each property leads to a call to Swift.isKnownUniquelyReferenced and can trigger .copy() of underlying RefBox which may be undesirable. A single call to Swift.isKnownUniquelyReferenced / RefBox.copy() is a more efficient pattern.

Hmm… if an engineer defines a CowBox struct with two (or more) mutable stored properties… and then defines a mutating "wrapper" function that mutates both properties… I'm not sure how this could lead to different behavior than if the two mutations had just been inlined directly… the first mutation attempted when the CowBox has a shared storage is what begins the copy:

@CowBox struct Person {
  @CowBoxNonMutating var id: String
  @CowBoxMutating var name: String
  @CowBoxMutating var location: String
  
  mutating func eraser() {
    self.name = ""
    self.location = ""
  }
}

let p1 = Person(id: "id", name: "name", location: "location")
var p2 = p1
p2.eraser()

Calling self.name = "" in this code leads to the isKnownUniquelyReferenced check against self._storage… that check leads to a new instance of _Storage assigned to the self._storage property. Calling self.location = "" then leads to the isKnownUniquelyReferenced check against self._storage. At that point self._storage only has one strong reference… it was just created. Which means we do not create a copy of self._storage (we don't have to).

There might be some concurrency issues to think through if a copy had been made "inline" with the execution of an asynchronous mutating func… but just from thinking through the synchronous example I don't see how this could ever lead to unnecessary copies being made.

mutating func can never mutate any stored property by design, but calling such function triggers get / set observers of enclosing instance, which can be used for making a copy.

I'm not sure I follow. The example is a function marked as mutating that does not actually mutate the property?

@CowBox struct Person {
  @CowBoxNonMutating var id: String
  @CowBoxMutating var name: String
  @CowBoxMutating var location: String
  
  mutating func eraser() {
    self.name = ""
    self.location = ""
  }
  
  mutating func nothing() {
    
  }
}

let p1 = Person(id: "id", name: "name", location: "location")
var p2 = p1
p2.nothing()
precondition(p1.isIdentical(to: p2))  //  true

This is ok… what would be the unexpected behavior with this example that would need to be guarded against or what additional functionality would need to be added before this behaves as expected?

It's also worth to mention properties with mutating get and nonmutating set for discussion.

The CowBox macro currently does not attach to computed properties (it only attaches to stored instance properties). I'm not sure this macro would make sense on a computed property… but if the community has more examples that this is important then we could discuss that.

If what you were suggesting is that a computed property (from before the CowBox was attached) could then touch the storage object reference directly… that behavior is not supported for now (the storage object reference is still considered private).

The nearest reference is @Observable macro where @ObservationIgnored is used as a marker when properties shouldn't be tracked.

I'm actually still a little confused about how the ObservationTracked should (or should not) be directly used by clients.[1] That conversation also contributed to me choosing to keep the CowBox property macros explicit for now. If we did move to making the Mutating property macro optional in the future (and make mutating property the default)… I would prefer to "not break" any code where an engineer chose to attach the Mutating property macro directly.

it allows to split implementation to several files
it allows to make extensions for _Storage class
in the same file when _Storage is fileprivate
in other files when _Storage is internal

Hmm… I'm still not completely sure I see the clear use case to enable engineers to access the Storage class (or storage property) directly. Is the missing functionality being able to directly force a copy of the storage from any arbitrary place (outside the setters where this copying is already enabled)?

If there was a specific use-case that needed the underlying storage to be accessed… my preference would be to expose extra functionality on the CowBox protocol (similar to how we expose isIdentical to check reference equality against the storage). Making that storage variable anything other than private isn't worth it IMO (since it means code outside the class can then touch it directly). I wouldn't even think of any reason for someone to touch the storage inside the CowBox struct itself (other than the from the codegen added from the macro)… but there's no formal way to enforce that AFAIK (other than the informal convention that the underscored variable should be considered off-limits).


  1. Unable to manually attach ObservationTracked. Is this intended behavior? ↩︎