[Pitch] Modify and read accessors

Hi Swift evolution,

We are proposing modify and read accessors for properties and subscripts. The proposal would formalize the underscored _modify and _read accessors that have been unofficially available since Swift 5.0.

These accessors allow copies to be avoided when changing and viewing a value's fields. As such, in their underscored form, they have been important in performance critical code for years. Now that noncopyable types are part of Swift, avoiding such copies is essential.

It has been nearly five years since the feature was originally pitched, and it has been available in its underscored form for even longer. As a result, many folks have a great deal of experience with this feature. That experience has informed how this proposal differs from the previous one. We're eager to hear how your experience with the feature fits with what we're proposing here.

The write-up is here in rendered markdown and here as a PR.

45 Likes

Would there be any examples that could be documented where get and set are still going to be the preferred accessors? Another POV would be if there are going to be use-cases where read and modify could be expected to hurt performance…

I guess a lot of product engineers would see read and modify as the "correct" default choice for accessors… which might be true from now on. Are there any more opportunities for expanding on that?

5 Likes

Thank you for improving upon the behavior of the existing _read and _modify accessors when the caller throws while borrowing the value — needing to use a defer to ensure consistent behavior was a really dangerous tripping hazard.

3 Likes

Would it be feasible to use the existing terms borrow and mutate instead of read and modify? The latter two seem to be synonyms of the former two.

It would result in confusing constructions such as nonmutating mutate or consuming borrow, but maybe that isn't terribly more confusing than nonmutating modify or consuming read.

3 Likes

I was going to say consuming get + read seems like a reasonable if rare combination for non-copyable types, but y’all included it in Future Directions already! Happy to go with that for now as long as there’s not a problem adding that coexistence later. (It certainly is a subtle difference on the caller side.)

Bikeshed names to avoid the source compat problem, somewhat inspired by ellie’s comment: yield get and yield set. This is not valid syntax today—no juxtaposition of identifiers is—and it’s pretty easy to search as a phrase. In prose I’d probably say “yielding accessor”. (I did try to get borrow, mutate, or inout in there, but I couldn’t justify it against borrowing get and mutating set, which, while disallowed for redundancy, are things you can write on a plain old func to describe self rather than the result.)

If not that, maybe still redundantly require borrowing read and mutating modify in existing language modes, and a future one could allow dropping that.

9 Likes

Glad to see this moving forward again!

One thing I'd like to see included in the proposal (and the swift book if accepted) is a section with guidance on when to use which accessors and why.
A lot of this is in the proposal already in various places but having a dedicated section with practical guidance would be an important part to refer back to after acceptance when wondering if and when to use these new accessors.

The proposal already hints strongly that get should be preferred over read unless one is solving concrete performance issues or needs to perform cleanup logic. That information should also be in the usage guidance section and expanded upon. When is read required (noncopyable types), is it possible to implement both get and read (no), ... Maybe mention how this might change the rationale for choosing get or read when using library evolution.

And the same for set and modify of course.

7 Likes

This is exciting!

One thing I think it would be important to figure out (at least in the abstract) before these become official is how these interact with async properties, and specifically with the current issues we have around isolation in the context of async properties (like @hborla discussed in this thread for functions).

Can either of these be async?

Can we take the opportunity to improve the design so if we do have an async read on a non-Sendable type you can actually use it from an actor-isolated context? For example, since these are new accessors, can we make them inherit the actor's isolation context by default (as opposed to having nil isolation?)

4 Likes

It looks like we are targeting 2024-11-13 for a 6.1 branch cut… I assume that means we are targeting 6.2 to ship read and modify? Is that correct?

Thanks for the feedback so far, everyone.

Several folks asked about when the various accessors should be used. That sounds like a handy reference, so we're putting together a comparison of them.

In the meantime, I want to address a couple of points:

This proposal is not targeting Swift 6.1.

Applying async to read and/or modify is not being proposed here. It is an interesting future direction, though. And when thinking about that future direction, it would make sense to consider the pain points of related features like those you mention.

These are great names for accessors. They are good candidate names for these yield-once coroutine accessors. And they might also be ideal for the kind of accessor mentioned in the "Borrowing a field" future direction in the document (briefly, such "projection accessors" would return a borrowed value which lives as long as the base object). As we consider spellings for these yield-once coroutine accessors, it might be worthwhile to consider also how such projection accessors might eventually be named.

3 Likes

I think what he was trying to say is that since it appears that 6.1 will be releasing in two weeks that we must instead be targeting 6.2 at the earliest.

I don't think that's correct. I just checked the thesaurus to confirm:read is not a synonym of get, and modify is not a synonym of set.

Actual synonyms would be something like fetch and put -- indeed, it would be fair to argue that we should not label new accessors with those specific words. However, read and modify aren't like that at all; and neither are borrow and mutate.

I think the names get and set are good because they hint at the instantaneous and final nature of these accessors. They are passing ownership to/from the caller -- no further cooperation is expected after the transaction.

The names read and modify hint at ongoing activities with a duration. These accessors provide temporary direct access to some entity, without transferring its ownership. I can (sometimes) read my friend's facial expression; I can let a locksmith modify my front door. Both of these activities are tied to a time interval with a specific start and end; neither of these activities transfer ownership of the thing being accessed.

We're planning to use the names borrow and mutate to name the (far more fundamental) accessors that provide direct in-place borrows and mutable borrows for "components" of self. Those accessors will generalize the semantics of how stored properties work in today's Swift, and they seem more deserving of those specific labels.

(The coroutine-based read and modify accessors are allowed/encouraged to materialize the yielded thing only for the duration of the access, which makes them unsuitable for modeling containment. The difference looks over-pedantic and extremely subtle until we try mixing these accessors with non-escapable types, where lifetime semantics suddenly become a crucial concern -- a matter of life and death, so to speak.)

Luckily, borrow isn't a synonym of read (nor get), and mutate isn't listed as a synonym of modify (nor set), either. True, these are all names in the same ballpark, but that should not be a surprise per se -- looking from a high-enough viewpoint, all accessors have the same purpose. But lexicographers seem to be saying these words have distinct enough meanings that our usage will not cause persistent confusion.

7 Likes

They were saying “read” and “modify” are (contextually) synonyms of “borrow” and “mutate”, not “get” and “set”. I don’t think your described planned use conflicts with that either, since that would be a top-level member and not an accessor, right?

4 Likes

D'oh -- apologies, @ellie20. I think my post does address your suggestion, but in an overly roundabout way.

To put it more explicitly: we want to reserve the names mutate and borrow for a third set of accessors that we want to add later, and I'm arguing that these names work best for those.

We know from years of experience that read and modify work as names for the coroutine accessors, because we've been using the old _read and _modify names for roughly the same accessor semantics. (Granted, I may be overly used to these, as I've been working with them since 2017/18 or so; however I did not ever find difficult to keep them separate.)

The names we give to accessors matters a great deal. I do agree that the names should not be too close to each other; but the names we're proposing (get/borrow/read and set/mutate/modify) aren't synonyms, and that makes them viable.

(Having three separate sets of accessors will definitely be more complicated than our current way, no matter what we name them. However, this seems unavoidable -- for Swift to successfully compete for the space previously filled by C/C++, we need to allow the language to properly model direct access; and this requires separating things that we could previously handle uniformly. Making coroutine accessors a public language feature doesn't really help the competitiveness part; it just resolves the issue that get/set aren't enough for noncopyable types. I expect the (far less general) borrow/mutate accessors will bring us the (predictable) performance and expressivity that we need.)

The only pair that gives me some pause is mutate and modify. These aren't synonyms either, but we've been using these somewhat interchangeably in loose contexts, such as in technical documentation; so using both of them may be stretching it a bit. "Mutate" implies a more fundamental change than "modify"; and that rhymes with the way the planned accessors will work -- but is it too late to separate the terms?

yield get and yield set aren't bad naming alternatives, but they feel a bit mismatched/patchy to me. ("yield set" feels particularly weird -- the modify accessor does not at all work like a setter.) However, if using these names helps reserving the borrow/mutate names for the upcoming direct accessors, then so be it.

What about yield borrow and yield mutate though? I think the coroutine accessors have semantics much closer to those than to get/set.

nonmutating is a indeed valid modifier on the proposed coroutine based modify accessor; however, consuming read is not a thing, and should not ever become a thing. (Consuming behavior only makes sense for a get accessor: consuming get gives us the semantics we often need when we want to unbox a wrapped value that is noncopyable. consuming read does not appear to have useful semantics. (Neither does consuming set, consuming modify nor consuming mutate.)

Note that the planned borrow/mutate accessors are not expected to support consuming borrow or nonmutating mutate, either.

4 Likes

Hmm...what if we committed to this scheme? As you describe it, the third pair of accessors are "direct," so—

get / yield get / direct get
set / yield set / direct set

4 Likes

Interesting; I don't hate it!

Note that "direct access to a contained item" may just be my own pet term to describe what borrow/mutate accessors would be for; I don't think it's a standard label, and some experts may object to it.

Will these compound naming schemes work well in protocol definitions? E.g., would people find this readable?

protocol Foo {
  var bar: Baz { direct get yield set }
}
1 Like

What are the direct accessors for? Is that the ones currently labeled unsafeAddress and such?

Those are the accessors alluded to here:

You can find a brief discussion of the immutable accessor here: [CoroutineAccessors] Pitch. by nate-chandler · Pull Request #2596 · swiftlang/swift-evolution · GitHub .

2 Likes

Yes, pretty much!

(The unsafe addressors have roughly the right shape, but I don’t think they are an exact fit, for two reasons:

  1. Technically they don’t implement the right lifetime semantics yet. They ought to return borrows (and mutable borrows) of things with the same lifetime as self, as they are for projecting a borrow of self into a borrow of an item contained somewhere within it; but currently they seem closer to behaving like coroutines, where the yielded item may only exist for the duration of the access. (I’m told this is a bug that is fixable.)
  2. It fundamentally does not look like a good idea for a language construct for implementing safe access to dictate the use of unsafe pointers. There needs to be a way to implement this form of direct access without having to take the address of anything. (Fixing this will require more than just correcting some implementation detail in the compiler.)

I expect the existing unsafe addressors to be able to fulfill protocol requirements that call for these new “direct” accessors; perhaps through shims, perhaps as is.

But unsafe addressors don’t seem great for building things. It’s difficult to compose them: for example, I think we have a pressing need to create constructs that forward direct accesses to other types, but an unsafe addressor cannot (elegantly) forward to another. (Certainly not like we can compose getters and read coroutines.)

I expect unsafe[Mutable]Address will work fine for our primitive vector types (UBP types, span types and Vector itself), where items are naturally accessed through pointers anyway. But we need a better design for modeling the general case.)

4 Likes

I'd rather we just stick with read and modify.

I don't like yield set — the modify operation is not a version of set with coroutines. It is an operation used for both reading and writing, whereas get is solely for reading and set is solely for writing. A more accurate term would be yield get set.

I also think read and modify are better because they're more familiar terms. Even though they aren't officially available, they are used in the source code of the standard library and in other prominent libraries (both official and unofficial) and they have a significant impact on ARC performance, so many Swift programmers are already familiar with the terms.

We could add read and modify without making source-breaking changes by adding an attribute:

@coroutineAccessors
var x: Int { ... }

@coroutineAccessors would indicate to the parser that read and modify can be used in a computed variable's body. (Of course, the actual name of the attribute can be debated.) If read or modify are used in a variable's body but the variable isn't marked @coroutineAccessors, then we could present a Fix-It that adds the attribute.

Alternatively, if we don't like the idea of an attribute that changes how a declaration is parsed, we could make coroutineAccessors an argument:

var(coroutineAccessors) x: Int { ... }

Of course, neither of these are ideal, but they're good enough to get us to Swift 7.


One pain point that has been brought up with the current _read and _modify accessors is that the fact that you can't yield from within a closure. Would it be possible to have an API that lets you work around this?

Maybe something like this?

var x: X {
    read {
        yield withCoroutineContinuation[] { continuation in
            lockedX.withLock { x in
                continuation.yield(x)
            }
        }
    }
}

I also think that the proposal should specify how modify works with property requirements declared on protocols.

2 Likes

I have found that the current implementation of read and modify in some generic contexts fail to optimize and allocate the coroutine on the heap. I’d suspect that the same wouldn’t happen if get/read would’ve been used [SR-11262] Array.subscript.read / Collection.subscript.read allocate · Issue #53663 · swiftlang/swift · GitHub

Yes, it would save a copy, but often a copy would be preferable to a malloc call.

If we are to see a more prevalent use of read and modify in user’s codebases we are likely to see performance pessimization. It would be hard to teach developers when get/set vs read/modify is preferable. The blanket statement of “just use new read/modify, and consider get/set legacy” is a lot simpler to teach and understand for the beginners.

2 Likes