Hi everyone! Thanks for providing feedback to our previous pitch for read and modify accessors. I've revised the proposal again based on all of the feedback we received. Since that pitch was initially posted, we've also begun preparing a broader vision for accessors, which has helped inform the design we want for these coroutine accessors.
Compared to the previous revision, we've made changes to the proposal including:
Contextualizing the coroutine accessors with respect to the accessors vision.
Adopting the names yielding borrow and yielding mutate for the coroutine based accessors instead of the traditional read and modify. Our hope is that these names better tie the accessors to their associated ownership modes (borrow for borrowing, and mutate for mutating) and provide a natural connection to future non-coroutine variations of these accessors that won't be yielding.
Expanding the "Implications for adoption" section with more discussion of when to favor these coroutine accessors over (or use them alongside) get and set.
The new revision of the proposal is here. Thanks for taking a look!
Currently we can have mutating get and nonmutating set to change the default about whether self is passed inout/mutating to the accessor or not.
So will we have nonmutating yielding mutate, mutating yielding borrow, and nonborrowing yielding borrow? How those words are juxtaposed makes it sound contradictory because it isn't clear that one word applies to self and the other applies to the yielded value.
I'd prefer to use these names instead yielding inout and yielding get. Changing the default treatment of self would then becomes nonmutating yielding inout, mutating yielding get, and nonborrowing yielding get. Sounds less like a contradiction.
I don't love the introduction of a new keyword (yielding) here, but I think I griped about that in a previous pitch thread so I won't lean into it too hard here.
An alternative might be to treat it like an effect, so you have get yields, borrow yields, mutate yields (or inout yields). By analogy, a body that can throw is spelled with throws.
The proposal doesn't clarify this but can you create asynchronous yielding accessors? If yes can you run asynchronous code in both before and after the yield?
I like the change that coroutines don't have to unwind but rather the code after the yield is run when the caller threw. Could it be a future extension to pass the error as inout to the coroutine accessor in some way so it can inspect/modify it?
I do like the idea of treating this as an effect. It opens interesting future directions like a method that can both yield and return a value for modeling streams of data with an end result. A common example of this is an HTTP request which has initial headers, potentially multiple body chunks and optional trailing headers. I know that this pitch is not about generators but it would be great if we could leave an extension to generators if we wanted to explore them.
I was thinking something along those lines (all hypothetical):
func handleHTTPRequest(headers: Headers, body: () async throws yields(RawSpan) -> Trailers?) {
let trailers = for try await bodyPart in body {
print(bodyPart)
}
print(trailers)
}
With this new nomenclature, what's preventing calling yielding borrow just borrow and avoiding the extra keyword? Or just mutate instead of yielding mutate?
This proposal does not support asynchronous yielding accessors yet. I haven't thought through whether we can in the future.
The idea is that borrow and mutate would be variants we add later which can return a borrowed or mutable value without needing to do work to clean up after the access ends (and, in the other direction, yielding get could produce a non-Escaping value which the caller receives ownership of, but whose lifetime is constrained by the coroutine execution).
It’s an extrapolation. inout on a parameter means that the parameter’s lifetime is equal to the lifetime of the binding from which the parameter was assigned.
I would also have previously said that it indicates that the callee has mutably borrowed the binding from the caller, except with my continuing confusion over between “yielding”, “borrowing”, and “lifetimes” I’m not sure about that anymore.
That's my thinking. It's less about the technical implementation and more about the abstract concepts a developer should be thinking of when they use these accessors. Tying the accessors to existing keywords may help to reduce the mental load developers take on when writing in Swift.
Minor suggestion: update the grammar to allow optional commas in protocol spec for properties. A { get set } is easy on the eyes; but changing { get yielding mutate set } to { get, yielding mutate, set }yields a clear readability win.
You can already separate the requirements with semicolons, like you can any declaration. The special case is that it isn't required between get and set in interface declarations.
Is that a special case? I thought It Just Worked because there's no ambiguity about which words are modifiers and which are accessor names. All of the following examples parse fine today:
protocol P {
var a: Int { get async throws set }
var b: Int { get nonmutating set }
var c: Int { get async throws nonmutating set }
}
Do we have any situations that require semicolons or a line break between body-less accessors?
Sorry, I mean that the special case is more generally that you don't need semicolons between accessor requirement declarations (get and set just being the most common combo). I think that using them would be more readable with multi-word requirement phrase, as @jaredgrubb suggested, but I don't think the introduction of yielding (or yields) would make it a necessity.
Makes sense! I was mainly trying to inform possible future swift-format work. As accessor modifiers become more prevalent than they are today, we might want to have a rule that requires them to be newline-separated instead of smashed together, and how semicolons play (or hopefully not) into that story.