SE-0507: Borrow and Mutate Accessors

I thought you were joking, but that code compiles! :slight_smile:

It is unfortunate that that phrase got into the language, forcing one to resort to a mental transfer. I don't understand why it wasn't called forwarding set or forwarding mutate.

@propertyWrapper
struct Wrapper {
    private let ref: Reference
    init(wrappedValue: Int) {
        ref = Reference(wrappedValue)
    }

    var wrappedValue: Int {
        get {
            ref.a
        }

        forwarding set {
        // forwarding mutate {
            ref.a = newValue
        }
    }
}
2 Likes

This is a minor point, and doesn't need to be addressed within this proposal, but

Cannot use for properties of classes or actors

Classes require runtime exclusivity checks to run both before and after each property access. Since borrowing accessors do not provide a way for the provider to run code after the access, they cannot be used for properties of classes.

All this means is that a borrowing accessor on a class instance can't return a stored property of self. It could still borrow from anywhere that supports static borrowing: a global or type-scoped stored constant, or (in theory, and unsafely!) from a pointer. Not super useful though.

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

For example,

struct Rectangle {
  var left, top, width, height: Double

  var right: Double { left + width }
  var bottom: Double { top + height }
}

Subscripts are a bit different because they're usually indexing into some underlying storage, so you usually want to propagate the borrow through if possible. And sure, computed properties might also just propagate some nested storage, conditionally return one stored property or another, and things like that. But computed properties that construct a new value are extremely common as well.

3 Likes

I see what you mean. I should have clarified that when I'm talking about computed properties producing a novel value, I'm talking about large values that are hard to copy.

In your example, for instance, I don't see a reason why the borrow getter would need to have a different ABI from the current “consume” getter. A Double is a single-word on 64-bit platforms and trivially copyable, so we should even be able to pass it directly through a register.


I also want to emphasize that my main concern isn't performance, it's fragmentation. Even if an additional copy is generated and the optimizer can't eliminate it, Swift is really good at handling that. In most cases, we'll copy a couple of words or issue some atomic increments and decrements; this cost is largely inconsequential. However, one of Swift's core strengths is that anyone can go to the docs, inspect the definition of a method, property, or subscript, and actually make sense of it.

By introducing distinct syntax for non-Copyable types, we fragment the Swift code into projects that care about non-Copyable types, and into apps that never need to touch them. The Standard Library will inevitably support non-Copyable types in more places, including Sequences and Collections. Given the ubiquity of Collections, it's easy to imagine a scenario where a beginner simply wants to see what a subscript returns, so they hover over the square brackets in their IDE and see a foreign-looking borrow. Of course, borrow is not a hard concept to grasp, but it clearly violates the principle of progressive disclosure. A beginner wants to simply learn about array indexing, yet they suddenly also need to learn about borrow vs get and Copyable vs ~Copyable.

1 Like

I want to mention that borrowing can also be used with Copyable types, and there’s a bit more to learn:

  • get + Copyable
  • borrow + Copyable
  • borrow + ~Copyable

This is at minimum.

I also want to note that ~Copyable is not an easy topic to learn. I’ve spoken with several iOS developers while trying to explain it, and here’s a list of the difficulties they’ve encountered:

  • What is it? What are move-only types, and why not just use reference types instead?
  • What does ~Copyable actually mean?
  • Why does ~Copyable have different meanings in type declarations vs generic constraints?
  • A tangential discussion on deep copies vs shallow copies.
  • Memory management and performance concerns.
  • Protocols and copyable types.
  • Dealing with mutability.
  • Ownership modifiers.
  • ... and so on.

In the end, they are left with the feeling of I’ve learned a lot, but understood little.
So, I can definitely understand the concern about fragmentation.

And for Copyable types, a copy can be created if the value needs to be consumed.

This is similar to usage of borrowing when dealing with ~Copyable types. Most of the time methods are borrowing, and it is a bit annoying to annotate them explicitly.
Borrowing by default seems to be the right choice for methods on ~Copyable types, getter accessors, and function arguments.

Collections subscripts also fit well with this Borrowing by default idea. Once collections support ~Copyable types, retrieving an element from a collection (like array[0] or dict[key]) would borrow the value. To consume the value, you’d need to use methods like Array.remove(at:) or Dict.removeValue(forKey:).

By default, we use let constants and only use var when necessary. This aligns well with the concept of borrowing vs. inout. Consuming is a distinct operation that should be explicitly specified and clearly highlighted.

Adopting borrowing by default everywhere seems like a consistent design choice across various parts of the language – methods, getters, function arguments, subscripts, switch statements and iterations.

Instead of introducing new accessors, I strongly support making the get accessor borrow by default, making possible consume get property for cases like your example

4 Likes

This sounds extremely ambiguous and contradictory. Is it mutating or not, finally?:)

1 Like

I don’t know if you’re actually asking or are just making a point about how confusing the syntax is, so I’ll answer both :slight_smile:.

Here, the “nonmutating” modifier applies to self and “mutate” applies to the return value. For instance, SwiftUI’s Binding has a wrappedValue whose setter is marked as “nonmutating” because “self” of type “Binding” is not modified. However, the setter gives us write access to the underlying “Value” type. Similarly, with “mutate” you’d get access to a sort of “inout Value” type that you can mutate (without mutating the container type).

If you were just making a point about how confusing “nonmutating mutate” is, I completely agree. Perhaps we should use the same terminology as function parameters “nonmutating inout” or directly specify self “nonmutating(self) inout”.

2 Likes

Exactly!
While I understand the intent behind it, the phrase nonmutating mutate feels quite absurd. nonmutating set might not be perfect, but it’s understandable. However, nonmutating mutate is redundant and creates confusion – how can something mutate without actually mutating? If we start naming things this way, here are some other terms that would be equally obscure:

  • writable readonly
  • synchronously asynchronous
  • flexibly fixed
  • permanently temporary
  • transparently opaque

I think we can agree these terms don’t communicate their purpose clearly and lead to more confusion than clarity. Let's be careful with terminology!

3 Likes

I think the point to draw here is that while borrow is incredibly clear and specific about the semantics of that accessor, mutate is very general. We talk about "mutation" all the time. There's nothing really in the word mutate that says "I'm returning a mutable reference to something" in the sense that borrow says "I'm returning a borrowing reference to something".

Unfortunately, we've already locked in these terms in other places:

  • nonmutating has long been the modifier we use to indicate that a function or accessor does not mutate the receiver
  • mutate is already the term approved by SE-0474 for yielding mutate, and it's important to keep the symmetry between the yielding and nonyielding versions of these operators.

All that being said, I think the concern about nonmutating mutate is overblown. That combination is likely to occur in only the most advanced of use cases. The average Swift user is not going to be running into types that return mutating references to storage outside of self (they might use them, but they're much less likely to be writing them). nonmutating set isn't very common and this would be even less common than that.

If we had been designing the entire set of accessors and modifiers holistically, we might have been able to avoid this, but given the state of the language today, I think what's being proposed here is fine.

6 Likes

Are we? We already have class that was renamed to AnyObject.

I'm not sure why there's such a rush with the development of Swift, but given that mistakes are often a part of this fast-paced process, what’s the reason against renaming mutate or nonmutating?

But does that mean those who aren't average Swift users should have to deal with unclear terms? I don’t think that’s a valid argument for introducing ambiguous terminology into the language.

I fully agree with this statement.

2 Likes

I think by the time someone reaches the point in their Swift life where they need this feature, they've probably already seen nonmutating as a modifier on functions and setters and understand that it refers to a declaration that doesn't mutate the receiver. Then they'll apply that knowledge to the mutate accessor.

The class constraint was deprecated back in 4.0, before we held language changes to a higher source compatibility bar. The nonmutating modifier is even older than that (I believe in the first publicly released version of Swift). So, sure, anyone can propose that we rename something that already exists, but the bar for those kinds of changes is much much higher nowadays.

If someone really wants to push on this, I think the most likely path would be to suggest renaming mutate and yielding mutate. While the latter has already been accepted, it hasn't made it into a released version of Swift yet (the status of SE-0474 is still just "Accepted", not "Implemented", so it's unclear if it'll make it into 6.3), so there may be time to make adjustments there.

But ultimately, we have to make the decision based on whether we think nonmutating mutate is confusing enough to warrant changing something that was already accepted.

2 Likes

It was removed, not renamed. It was redundant, because “protocol P: class” could also be spelled as “protocol P: AnyObject”, and “AnyObject” was meaningful in more places than just the inheritance clause of a protocol.

1 Like

It's still just a warning in nightly compilers, even in Swift 6 language mode.

<source>:1:13: warning: using 'class' keyword to define a class-constrained protocol is deprecated; use 'AnyObject' instead [#deprecation]
1 | protocol X: class {}
  |             `- warning: using 'class' keyword to define a class-constrained protocol is deprecated; use 'AnyObject' instead [#deprecation]
4 Likes

This cannot work when `i` is nil, since in that case, there is no stored String? that can be borrowed.

thanks, that's interesting and i suppose it makes sense. out of curiosity, is it conceivable to ever support something along these lines? i was thinking maybe you could have a special immortal storage reference for the nil case, but not sure if that's actually coherent given the model here.

Yeah, that could eventually be made to work. Another alternative that’s likely to be supported much sooner:

    var prop: String? {
        borrow { i!.s }
    }

1 Like

ah yeah i was going to ask about that. going on a tangent a bit, but if force unwrapping were to work, then if multiple return statements were also supported, in theory could you do this by returning a different storage reference for the nil case? e.g.

let nil_string: String? = nil

var prop: String? {
  borrow {
    switch i {
      case .some: return i!.s
      case .none: return nil_string
    }
  }
}

Could you not use return switch { … } syntax?

This proposal has been accepted with some clarifications. Thank you, everyone!

Doug

4 Likes