SE-0507: Borrow and Mutate Accessors

I’ll elaborate on my pitch thread comment (I apologize for not replying on that thread).

My main concern with the current proposal is that it creates different defaults for non-Copyable types, further exacerbating the dialect split between Copyable and non-Copyable Swift. In practice, a lot of borrowing code, including in the standard library, will have to use different syntax for subscripts and computed properties. Thus, I'm worried that this proposal:

  1. breaks the intuition that properties/subscripts expose something a type owns,
  2. goes against Swift’s progressive disclosure principle, and
  3. ultimately hinders adoption of non-Copyable Swift.

@allevato Could you elaborate on what these use cases where we construct a new value are? As I explain below, I think most people’s intuition regarding computed properties and subscripts is that they’re views into the source type, so most don’t really construct a new instance.

Changing the Default

Intuitive Semantics: Computed properties look like stored properties because they’re supposed to expose a property that a type already owns, sometimes with small modifications. Similarly, subscripts expose a view into data that the type owns. For instance, rawValue tends to be (or expose) a property of the type’s internal representation; similarly, Collection subscripts expose views of the type, either returning a subcollection or an element that the collection still owns. Of course, there are exceptions: sometimes a computed property / subscript will construct an entirely new value simply for the caller that the type itself doesn’t use internally, e.g., a complex description string that concatenates other strings stored in the type.

If an API author deems that a property/subscript should return an owned value, they would still be able to change the property to something like a consume get (that doesn’t just borrow the return value). However, creating a consume get property should be the exception, not the rule. Functions are the de facto way of producing a new, owned value, e.g., UnsafeMutablePointer.move(), Array.map(_:) and most non-lazy higher-order functions.

Reducing Dialects: Currently, it feels like a whole new language is developing around non-copyable Swift with borrow switch, borrow properties/subscripts, etc. For most computed-property and subscript getters, developers want to return a borrowing return value. Hence, creating distinct syntax unnecessarily complicates syntax to preserve the status quo.

Reinforcing this dialect difference not only hinders the adoption of non-Copyable types but also goes against Swift’s principle of progressive disclosure. As the Standard Library starts to support more non-Copyable types, it will become necessary to expose non-Copyable types through properties and subscripts. As a result, beginners may look at the Standard Library docs and see that a simple Collection type’s property has a borrow.

Source Compatibility: Minimal breakage. The vast majority of Swift types remain Copyable, so if get returned a borrowing ReturnValue (per my proposal), the callee would get the borrowed value and only generate a copy if required. As I discuss in the next section, this comes with performance benefits for libraries. The only breakage would occur when dealing with non-Copyable types, where a getter can currently produce a consuming ReturnValue. Under my proposal, existing users who rely on an owned instance instead of borrowing said type would get a compilation error. However, these use cases are arguable antipatterns. The only way to return a consuming MyNonCopyable is if the getter initializes MyNonCopyable or if it consumes MyNonCopyable from a source such as self. In both scenarios, an unassuming getter has the unexpected side effect of creating MyNonCopyable, which is usually ~Copyable precisely because it’s an expensive resource to manage.

Performance: Libraries, especially dynamically linked ones, could benefit from changing the default behavior of the getter to be borrowing. While whole-module (and sometimes cross-module) optimizations eliminate copies where possible, the compiler can’t make assumptions about external libraries. So if a library type exposes an internal string through description that the user only prints (a borrowing operation), then we’d eliminate an unnecessary acquire/release pair. Because most getters expose existing types, reducing copies and ref-counting traffic can add up.

But what if a property/subscript constructs a large owned value solely for the callee, and we end up just borrowing that value? If that property witnesses a protocol requirement where we interpret get as a borrow, there’s not much we can do to make this more efficient. For instance, consider a type StringPair that contains the properties a,b: String and witnesses CustomStringConvertible’s description with a computed property that returns a+b.

ABI Impact: The ABI is primarily a concern for Apple platforms, but we can mitigate ABI breakage with thunks. Libraries compiled with Swift Evolution and that support versions of Swift would generate a thunk that calls the borrowing accessor, copies the return value, and returns that. This solution makes the (reasonable) assumption that no dynamic libraries ship with a getter that returns a non-Copyable type.


Of course, I understand that my proposal requires significant technical changes to the language, but I wanted to express my concerns about the proliferation of ~Copyable-specific syntax that unnecessarily hinders adoption and confuses beginners by going against Swift’s progressive disclosure principle, while also breaking our intuition about getters.

2 Likes