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:
- breaks the intuition that properties/subscripts expose something a type owns,
- goes against Swift’s progressive disclosure principle, and
- 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.