protocol P {
var foo: Int { get; set } // ERROR: Expected get or set in a protocol property
}
Bonus points if it's possible: generated swift-interface should default to use the separator (, or ;) if there are multiple-word accessors in the list.
I am very excited to see this progress. It addresses an important gap in expressivity.
My main qualm is on the naming. My view is yielding borrow and yielding mutate is way too verbose a name for what I suspect will become increasingly important implementation technique, assuming uptake of things like noncopyable types and other evolving techniques continues. Both a double-keyword and the gerundive yielding gives this feature a waffly feel that I fear will be off-putting to many and flag it as an "advanced" feature, which I do not think it is given what is written in the when to favor coroutines section of the pitch.
I understand the desire to make this about combinations of words, which compose nicely. But that can't take a back seat to actual usability (and I understand that many folks believe verbosity increases readability – but I disagree). I would like us to try harder to establish concise naming for this feature.
I am unsure if the Windows implementation for this was added. I know that there was at least one thing that I noticed in one of the PRs related to this: we needed name decoration support for Windows, which requires working with Microsoft (or we need to replace the name decoration for one of the other previously created calling conventions).
I think that this type of change is large enough to warrant that the Windows target to be supported before this is accepted.
The current implementation of coroutine accessors has been implemented for several years at this point. We can continue to use it on platforms that don't yet support the more efficient coroutine implementation, so if that support requires vendor coordination on some platforms, we can hold off on transitioning to the new implementation.
I guess I need to look more into the details, but I am concerned that it may require ABI changes once accepted (e.g. the concurrency support on Windows ran into issues with fat pointers). I think that we should have at least a PoC implementation on Windows to ensure that such issues are identified even if we do not enable it by default yet.
We don't yet have the non-yielding variants of borrow and mutate, but looking toward that direction, it would be interesting to see how many uses of _read and _modify in the wild actually need to leverage their coroutine-ness. My impression is that most _reads don't do any interesting cleanup. In the case of _modify, the standard library does have a couple of cases of interesting cleanup:
Dictionary.subscript(_:)._modify uses the coroutine to vend a temporary Optional by moving the value out of the dictionary to prevent a copy, and then uses the coroutine resumption to either move the value back or implement the delete-on-nil behavior.
*Array.subscript._modify uses the end of coroutine to emit an endCOWMutation() marker for the optimizer's sake. This has no runtime effect, and we could perhaps model the operation in a different way that would allow a simple mutate return to work.
Set has an internal _modify accessor that uses the coroutine resume for Foundation bridging purposes.
String.unicodeScalars._modify uses coroutine resume to move ownership of its buffer in and out of a UnicodeScalarView to avoid copying the buffer. This pattern of use may ultimately be better handled by having view-style types be ~Escapable so that they can reference other values' storage without retaining.
Other uses of _modify in the standard library only yield a value with no defer-ed or other further operations when the coroutine resumes. I have a sense that, when we do get the non-yileding mutate and borrow accessors, those may yet end up being the more common option, in which case putting the extra keyword on the yielding variants seems like the better long term choice. If yielding accessors end up being relatively rare overall, then it also seems preferable to have it be a modifier times four core accessor kinds (get, set, borrow, and mutate) rather than have seven different concise names for developers to memorize.
We don't make coroutine-ness a modifier and just pick completely different keywords for yielding and non-yielding accessors. The big problem here is that the basic semantic model from the outside is almost the same, so we're more-or-less forced to pick synonyms. We had that at some point — it was mutate vs. modify and borrow vs. read, or something to that effect — and it was awful even for those who were experts in the idea.
We add a pair of modifiers which explicitly opt in and out of coroutine-ness (yielding and nonyielding, say) and force these accessors to always be explicit. Very clear, very verbose.
We make these accessors default to coroutine-ness and add a modifier that opts out (yielding).
We make these accessors default to non-coroutine-ness and add a modifier that opts in (nonyielding).
The choice between 2, 3, and 4 basically comes down to how you evaluate the trade-off between achieving optimal performance and reserving future flexibility. Non-coroutine mutate and borrow achieve very strong guarantees at the cost of very tight constraints on the implementation. A concrete type that guarantees non-coroutine accessors is likely to end up locked in to its current implementation. An abstract type (like a protocol) that guarantees non-coroutine accessors is consciously ruling out whole categories of possible implementations. That is all perfectly fine for a high-performance data structures library, or a protocol that's deliberately chosen to only generalize over high-performance data structures. However, it's probably not something that other kinds of APIs should ever be promising unless they're willing to outright freeze their type.
I'm not totally sure yet which of these options I prefer. (4) seems like the most consistent with our general principles: a tighter constraint requires a more elaborate spelling. But I'm also aware that a lot of the code that's going to want to embrace these features will just be writing data structures, for which a coroutine might become a performance footgun.
The problem with that is that it becomes a source and ABI breaking change if you do need coroutines later. And coroutines are the general case so it is quite possible you will later need one.
Version 1:
struct Foo {
var x: HasRoutineSpanAccessor
var span: Span<Int> {
x.span
}
}
Version 2:
struct Foo {
var x: HasRoutineSpanAccessor
var y: HasCoroutineSpanAccessor
var span: Span<Int> {
someCondition ? x.span : y.span // ERROR - this must now become a coroutine
}
}
Even though in both cases I'm trying to return a Span, I suddenly need to care about which accessors x and y (Foo's implementation details) make available, and it is possible they will be incompatible with the interface Foo has already established.
This is yet another dimension of complexity that developers will need to grapple with, both library clients and developers, and I wonder how well they will manage it. The complexity that has recently been added to the language has made it much harder to use, both for experts and newcomers, and I am wary about this change because it also adds a deceptively large amount of complexity.
(I realise that this echoes a lot of what John said, but it's been on my mind ever since the accessor manifesto thing was revealed so I wanted to say it again in my own words regardless)
For source libraries, I don't see why the optimiser couldn't eliminate any difference between the routine and coroutine versions. For binary libraries, I wonder if careful splitting of functions and use of inlining could achieve the same effect (allowing the developer to promise a no-op cleanup block while keeping the rest of the implementation private) while allowing the accessor to technically remain a coroutine.
Not necessarily. The library evolution ABI for copyable types already abstracts away the precise set of accessors used by a property, revealing only get, and for mutable properties, yielding mutate and set. Internally, the property can change implementation from shared to computed get/set, and from only get/set to the full get/[yielding] mutate/set, without breaking ABI. If the type or property is frozen, one could still provide the yielding mutate as a backward compatibility entry point while providing a non-yielding mutate as a more efficient ABI for new code.
The story is somewhat more complicated for noncopyable types (for which borrow vs get is a fundamental semantic difference) and for nonescapable types (for which yielding vs returning affects the maximum lifetime bounds of the result), but given the fundamental interactions there, that seems unavoidable.