If not going with the Reference approach, I do wonder if borrow and mutate can be reasonably seen as special cases of read and modify (which would flip/resolve some of the naming). In both cases the access to the base extends until the access to the property has completed; the borrow/mutate case merely has an additional guarantee which lends it additional powers: the “cleanup” is guaranteed to be a no-op. We still want to give mutate a better API than modify, but maybe we can call them mutate and direct mutate instead of modify and mutate. Then people only have to learn to write direct mutate (and direct borrow) if
they want to ensure that this lifetime can be extended, or
they are writing an ABI-stable library and want to make performance guarantees
For a non-stable library, the compiler can infer whether any cleanup is actually necessary, and record that in the binary module interface.
I don’t think I’m the right person to come up with a real spelling, but it’s neither unchecked nor unsafe. The actual implementation guarantee in my view of the model is something like “sans writeback” or “auto-writeback” or “in-place”; the resulting semantic guarantee is whatever term ends up getting used for lifetime transitivity overall. We could hold off on naming it for that reason; after all, this pitch does not propose to introduce those accessors just yet, though we will eventually want them.
That's because our ideas there are still evolving. Today's implementation usually keeps the coroutine scope very short, which can cause copies to be inserted in order to preserve values beyond that scope. But the optimizer should someday be able to dynamically compute the ideal duration for the coroutine by analyzing the code usage and exclusivity constraints.
The coroutine-based accessors are about more than just type mismatches. They support any case where a value may need to be presented with a different in-memory layout. For example, there are a number of types that are expressed as an enum where each case holds a different representation of the data -- a compact inline form vs. a pointer to expanded data on the heap. A coroutine accessor can present those varying forms consistently by temporarily transforming one of the representations into the other, insulating clients from such internal variation.
As for borrow, it is certainly possible that it might end up becoming a get that returns an explicit reference rather than a separate kind of accessor. So far, I feel like the considerations explored in that Vision document -- supported by experiences with languages like C++ and Rust -- do show the need for some form of borrowing accessor, but I'm not yet convinced we know what shape that should take. That document lays out one possible shape as a starting point for discussion, and I'm glad to see folks here sharing other ideas. But we still have a lot of work remaining to build out all the core infrastructure for fully-functional nonescaping types and lifetime controls before we're ready to prototype these approaches.
They should probably be at least warned about, yeah.
Yep. I was mostly just trying to motivate why some folks have suggested inout as an alternative name for what I called mutate in that document. But you're quite right that "reverse inout" could be used to describe other approaches.
Keep in mind that, long term, we want programmers to opt into coroutines deliberately, only when they know they actually need code to run dynamically after the access completes. For reads, a coroutine is almost never the right choice; we want the non-coroutine borrow accessor to be the obvious default when programmers want to avoid copies.
yield is the perfect name for "read coroutine". It is unambiguously associated with coroutines and clearly implies a borrow. It does not add any new terminology, and programmers know exactly what their asking for and why. Searches will only turn up coroutine declarations and their implementations [aside from Task.yield()].
The same keyword will also tie accessor coroutines to yield-once coroutines, which I imagine being spelled: func foo() -> yield T
yield inout is the natural choice for a "modify coroutine".
borrow/mutate are good names for projection semantics. They clearly indicate direct access without producing an independent values. As John mentioned, these really do not have getter/setter semantics.
Sorry I don't have any experience with this feature but I have a question: can it be used to extract an enum's associated value without copying it? or with less copying?
Like this:
enum Configuration {
case small(Small)
case large(Large)
var optionalLarge: Large? {
_read {
switch self {
case .small:
yield nil
case .large(let associatedValue):
yield associatedValue
}
}
set {
if newValue != nil {
self = .large(newValue!)
}
}
}
struct Large {
/* Many array/dictionary properties, possibly expensive to copy */
}
}
Should I use the read keyword like this?
It compiles but seems strange.