Ownership adoption case study: building a wrapper type

Hi, folks, I've been trying to adopt ownership features in my project, and during the work, I've found several points in the language that are holding me back. I want to share a case from my experience here.

Motivation

There exist many types in first-party libraries, and people often extend them using extensions. But extending those types directly can easily introduce naming collisions. What we want to build is a general wrapper type that can be used to implement "entry points" that extend existing types.

Instead of writing this:

// utility library
extension String {
  mutating func extendedOp()
}

// clients
var str = "hello"
str.extendedOp()

we can write this:

// infrastructure
struct MyExtensions<T> {
  let core: T
}

// utility library
extension String {
  var myExt: MyExtensions<String>
}
// now all the extensions happen on this wrapper type
extension MyExtensions where T == String {
  mutating func extendedOp()
}

// client code
var str = "hello"
str.myExt.extendedOp()

I often call such myExt an "umbrella" property.

Original Implementations

Years ago, we implemented the above types using simple getter/setter pairs, and to improve the performance, we even tried using coroutine accessors.

// take 1
extension String {
  var myExt: MyExtensions<String> {
    @inlinable @inline(__always)
    get { MyExtensions(core: self) }
    @inlinable @inline(__always)
    set { self = newValue.core }
  }
}

// take 2
extension String {
  var MyExt: MyExtensions<String> {
    @inlinable @inline(__always)
    get { MyExtensions(core: self) }

    @inlinable @inline(__always)
    yielding mutate { 
      var ext = MyExtensions(core: self)
      yield &ext
      self = ext.core
    }
  }
}

Of course, these two methods are not optimal in terms of performance. Now, with more and more ownership features becoming available, we hope the new features can help in this situation.

Exploring a new implementation

One thing we always knew but could not express is that we expect our users to only use MyExtensions in an "ad-hoc" way, and every MyExtensions instance should just be a "view" of the original subject, this fits perfectly with the "non-escapable" concept.


However, here comes the first drawback: we have to implement 2 wrapper types, one for borrowing semantics and one for mutating semantics.

struct MyExtensions<T>: ~Copyable, ~Escapable {
    let core: Borrow<T>
    @_lifetime(borrow core: T)
    init(core: T) { self.core = Borrow(core) }
}

struct MyMutableExtensions<T>: ~Copyable, ~Escapable {
    let core: Inout<T>
    @_lifetime(&core)
    init(core: inout T) { self.core = Inout(&core) }
}

And every extended type have to implement two different properties to vend the views:

extension String {
  @_lifetime(borrow self)
  var myExt: MyExtensions<String> {
    MyExtensions(core: self)
  }

  var myMutableExt: MyMutableExtensions<String> {
    @_lifetime(inout self)
    mutating get {
      MyMutableExtensions(core: &self)
    }
  }
}

Originally, the clients can call the single "umbrella" property and the compiler will automatically pick the correct accessor:

var str1 = ""
_ = str1.myExt.nonmutatingMethod()  // âś… invokes myExt.getter
str1.myExt.mutatingMethod()  // âś…  invokes myExt.getter + myExt.setter, or myExt.modify

But now the client have to explicit write down the correct property:

var str1 = ""
_ = str1.myExt.nonmutatingMethod() 
str1.myMutableExt.mutatingMethod()

This ergonomic loss is very much analogous to standard library types that vend both Span and MutableSpan.


The second drawback is that the above code actually contains a line that cannot compile at the moment:

str1.myMutableExt.mutatingMethod() // ❌ sorry, but not working

The reason is very clear, because mutatingMethod requires self to be mutable. However the self instance is just an rvalue returned from myMutableExt, it cannot be mutable.

This severely affects the usability of our "umbrella" property, we cannot imaging forcing our clients to first write var myExt = str1.myMutableExt every time. This drawback is very much analogous to the problem MutableSpan and Inout suffer, hopefully it can be addressed by the exclusive pitch.


Just as a side note, the best workaround I can find to overcome the first issue, is to change my myExt property into methods, because unlike properties, methods can overload on return types:

extension String {
  @_lifetime(borrow self)
  func myExt() -> MyExtensions<String> {
    MyExtensions(core: self)
  }

  @_lifetime(inout self)
  mutating func myExt() -> MyMutableExtensions<String> {
    MyMutableExtensions(core: &self)
  }
}

For better or for worse, we expect the clients can write like this

var str1 = ""
_ = str1.myExt().nonmutatingMethod()  // âś… invokes func myExt() 
str1.myExt().mutatingMethod()  // âś… invokes mutating func myExt() 

Conclusion

From this case, I gain the impression that it might not be a good idea to use ownership features when implementing a wrapper type (at least for now). However, to some degree, the idea behind this case is general-purpose enough: I just want to build custom efficient "views" for existing types.

I would be very glad if you guys can tell me how you would do in such situations.

5 Likes

str1.myMutableExt.mutatingMethod() // :cross_mark: sorry, but not working

We always planned to fix this but haven't committed to a solution. @John_McCall alluded the "re-borrowing" approach here: `exclusive` parameter ownership modifier - #23 by John_McCall

While I don't think it's been pitched yet, I like everything about it... except the name :) It should be called the "exclusive-copy" approach.

The Mutable/Immutable type dichotomy is annoying but pretty fundamental to Swift ownership as @John_McCall explained here:

I’ll leave it to others to suggest struct-extension alternatives.

2 Likes