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.