Encapsulating Value Semantics, Part 3

4 minute read

In my last post on this topic, we built up a small struct named CopyOnWrite that abstracts away the need to manually check whether or not a reference needs to be copied before modifying it to preserve a type’s value semantics. As of last post, the type looks a bit like this (after some small tweaks):

// Box fits best nested within the `CopyOnWrite` type, but swift 3.0
// unfortunately doesn't support types nested inside generic types.
fileprivate class Box<T> {
  let unbox: T
  init(_ value: T) {
    unbox = value
  }
}

struct CopyOnWrite<T: AnyObject> {

  private var _reference: Box<T>
  private let copy: (T) -> T

  init(reference: T, copy: @escaping (T) -> T) {
    self._reference = Box(reference)
    self.copy = copy
  }

  var reference: T {
    return _reference.unbox
  }

  var mutatingReference: T {
    mutating get {
      // copy the reference only if necessary
      if !isKnownUniquelyReferenced(&_reference) {
        _reference = Box(self.copy(_reference.unbox))
      }

      return _reference.unbox
    }
  }
}

What we have so far is pretty nice, but it’s still a bit flawed. What if we wanted to add multiple properties of the same type to a struct? We would be forced to provide the same copy closure each time we create an instance of CopyOnWrite:

struct Counter {
  private var _primary = CopyOnWrite(reference: Foo()) { old in
    let copy = Foo()
    copy.count = old.count
    return copy
  }
  private var _secondary = CopyOnWrite(reference: Foo()) { old in
    let copy = Foo()
    copy.count = old.count
    return copy
  }
  // ...
}

Yuck! We have to provide the exact same copy closure each time we create an instance of Foo, which isn’t good. So, let’s extract this copying behavior into its own method on Foo:

extension Foo {
  func clone() -> Self {
    let copy = self.init()
    copy.count = self.count
    return copy
  }
}

struct Counter {
  private var _primary = CopyOnWrite(reference: Foo(), copy: { $0.clone() })
  private var _secondary = CopyOnWrite(reference: Foo(), copy: { $0.clone() })
  // ...
}

That’s better, but we have to specify the closure that wraps clone each time, which still feels like needless repetition. To solve this, we can create a Cloneable protocol to represent all copyable types:

protocol Cloneable: class {
    func clone() -> Self
}

(I chose Cloneable as a name because I didn’t want the method name to conflict with NSObject’s copy() method.)

We constrain the Cloneable protocol to only be applicable to classes, since structs and enums already are copied by default when you assign them to new variables, so it wouldn’t make much sense for those types to conform to it.

Now, we can extend CopyOnWrite for all cases where T conforms to Cloneable to provide a new, simpler initializer there. This will make assigning value semantics to a reference in a struct much easier if that reference conforms to Cloneable:

extension CopyOnWrite where T: Cloneable {
    init(reference: T) {
        self.init(reference: reference, copy: { $0.clone() })
    }
}

For completeness with existing Foundation types that implement NSCopying or NSMutableCopying, we can add similar extensions for those protocols:

extension CopyOnWrite where T: NSCopying {
    init(copyingReference reference: T) {
        self.init(reference: reference, copy: { $0.copy() as! T })
    }
}

extension CopyOnWrite where T: NSMutableCopying {
    init(mutableCopyingReference reference: T) {
        self.init(reference: reference, copy: { $0.mutableCopy() as! T })
    }
}

(The init argument names need to be different, otherwise you’ll get an ambiguity error when you try to use a type that conforms to more than one of these protocols.)

And now with this, we can finally clean up the original example:

class Foo: NSObject {
  var count: Int = 0
}

extension Foo: Cloneable {
  func clone() -> Self {
    let copy = self.init()
    copy.count = self.count
    return copy
  }
}

struct Counter {
  private var _primary = CopyOnWrite(reference: Foo())
  private var _secondary = CopyOnWrite(reference: Foo())

  mutating func increment() {
    _primary.mutatingReference.count += 1
  }

  var value: Int {
    return _primary.reference.count
  }
}

If we have to add many properties that hold values of the same type, we just have to conform the type to Cloneable (or if you like, NSCopying/NSMutableCopying) and then just wrap the instance in the CopyOnWrite struct. That’s much easier to read and maintain, isn’t it? 😄

Limitations

I mentioned this previously, but I wanted to revisit it here. While CopyOnWrite does some nice things, it doesn’t protect you from improper use of the reference type you’re wrapping. There’s nothing we can leverage within the Swift type system that can stop you from using the non-mutating reference for a mutable operation, nor can it stop you from using the mutating reference when you don’t need it (causing needless copies of your referenced data). It’s still up to you, the user of this API, to know how the types you’re working with behave so that you can use them correctly with this wrapper type.

Conclusion

We’ve done a lot to extract all the repetitive boilerplate code into a reusable component that can be applied anywhere you want to have value semantics in your structs, while achieving the performance benefits that come with copy-on-write behavior. I hope that you might find this useful, and if you have any feedback please feel free to contact me on Twitter or via email. I have a gist of a tweaked version of the final code for CopyOnWrite available here as well if you want to use this in your own project.

Until next time!

Leave a Comment

Your email address will not be published. Required fields are marked *

Loading...