Can _read be syntesized from _modify?

Most of the time _read and _modify accessors are written almost identically, except for the inout-ness of yeild operand. Could we use this fact to make _read optional and synthesize it via _modify by dropping inout-ness?

var f: Type
var p: Type {
  _modify {
    yield &f
  }
  // syntesized _read pseudocode
  // _read {
  //   yield _modify()
  // }
}
3 Likes

The primary benefit of coroutine accessors such as _read and _modify is that they avoid copies.

Until very recently, all types in Swift were copyable, and the only thing we had to worry about was that copies that were not eliminated might make data non-uniquely referenced and trigger unnecessary COWs. For this reason, the compiler does already synthesise _modify in many cases (e.g. for stored properties).

Example - note the presence of the output.Foo.bar.modify symbol in the generated output.

In more complex cases involving copyable types, the benefit of _read over making a copy is less clear, even if _modify is defined for efficient mutations. It isn't always possible to generate an efficient reading accessor from an efficient mutating accessor, and there is a negative impact on code size. You can pair _modify with get and set; you are never required to implement a _read for a copyable type.

For non-copyable types, coroutine accessors are all you have, so the compiler generates both _read and _modify accesors. Example.

_modify can mutate self at any time, not just at the time of yield. asking the compiler to synthesize a non-mutating version of it is like asking the compiler to synthesize a non-mutating version of a mutating instance method.

when both _modify and set are available, how does swift decide which accessor to call?

From https://github.com/apple/swift/blob/main/docs/OwnershipManifesto.md#generalized-accessors

It may also choose to define both , in which case set will be used for assignments and modify will be used for modifications.

2 Likes

We have talked before about wanting some way to synthesize paired get and set when they’re basically the same, so we could introduce a rule for “you defined modify without get or read, can we share the body”, or perhaps “you wrote readOrModify”. With macros, though, we now also have a way to experiment with this sort of thing; though I haven’t done it, I believe it’d be possible to implement this, at least in simple cases:

@ReadAndModify(\Self.f)
var p: Type

// Not sure about this one
@ReadFromModify
var p: Type {
  _modify { yield &f }
}
2 Likes

I'm experimenting with this, so far I've found a way to implement _read via _modify by hands without excessive copies. But I'm not sure if @attached(accessor) macro plays nicely with an existing _modify.

PS still need a better way to convince the compiler to drop ownership of the moved t without strong_release

That isn’t correct at all; you’re destroying the original stored value every time you read. (Put all of this in a function to see it crash; being global variables is keeping the objects alive past the end of the program. Put all of it in a struct and mutating/nonmutating will show you it’s broken.)

In general you cannot literally have a read that delegates to a modify because the compiler can’t see from one accessor into the other, which means you can’t get mutating to match up. If you go down this route, it might appear to work for classes today, but it won’t once we have borrow variables, because then you’ll have a mutating borrow on the underlying stored property when you only wanted a shareable, non-mutating one. You have to duplicate the body, or use a key path or similar to factor out some of the work.

4 Likes

Yep, I see, I was wrong. Thanks.

And I still believe that there might be an approach to generalize all types of accessors. In a broad sense, init, get, set, read, modify only use:

  • transformation from the underlying property type and the result property type
  • inverse transformation
  • some way to access the underlying property

Yeah, this is possible at the compiler level. The language just doesn’t expose a way to do it correctly, even unsafely: you need to be able to get a mutable pointer to the location without actually accessing it. And you lose exclusivity checks if that’s all you do, so you also want to make mutating accesses count as real accesses for class properties and globals, which the language does not expose separately.

I looked at how the synthesis of these accessors works in Val. link
Looks like a good solution for most cases.
In a nut shell, there are three accessors: let(for reading), set(for assigning) and inout(for modifying). And inout can be synthesized from set and let.

1 Like