It's true that's how yield (or synonyms) tend to work in other languages - does it that have to be that way, though? Couldn't Swift say that the coroutine may require that it be resumed cleanly, without exception (pun slightly intended)? Presumably this could be enforced by the static type system, whether through 'throws' or its absence, or some similar mechanism specific to yield…
e.g.
func modifyA() yields -> inout T {
// Prepare stuff.
yield &_value // Always returns & resumes.
// Finish stuff. This is _always_ run.
}
func modifyB() yields interruptably -> inout T {
// Prepare stuff.
guard yield &_value else { // Compiler requires use of guard now,
// to handle interruptions.
// Cleanup.
return // Or could throw in a variant of this method marked throws.
}
// Happy path. Only run if control resumed cleanly.
}
modifyA() += 5 // Fine, no possibility of different control flow
// short of process abort.
x = modifyA()
throw Error.Reasons
x += 5 // Compile-time error: "x", returned by coroutine `modifyA`,
// may only be used before throwing or returning.
y = modifyB()
if maybe {
return // Fine. `modifyB` is implicitly resumed into the else
// clause of the guard, before the `return` here takes effect.
}
y += 3
It's worth noting that a lot of this is only a concern with mutable values in combination with yields. I would expect that most code will yield immutable values, and wouldn't care how the caller resumes the coroutine; it's only critical to know when you have to know if the caller finished initialising your mutable value or not.
Using defer for this is similar to writing a distinct finalise method on the coroutine object itself (which is how Python makes you do it, for example). But, as in Python, that means you have an ugly hole in the language where you either can't because the object is implicit or can easily forget to because the compiler doesn't make you do it correctly.
An alternative would be to say that the code after yield is always run - it's implicitly 'defered'. That doesn't solve the problem, however, of distinguishing between whether the yield actually worked as intended or not, which is critical for yields of mutable values.
I feel like trying to express this through the existing function return syntax is problematic, since if you want to say that the code yielded to may throw an exception, where do you put the throws?
func modify() rethrows yields -> throws inout T { // Wat?
do {
try yield &_value
}
}
Maybe the stuff relating to the yield should be in the parameters…
func modify(_ yield: @yield (inout T) throws -> ()) rethrows {
do {
try yield(&_value)
}
}
Now it's at least clean, and the only magic here is this new @yield decorator, similar in some sense to @autoclosure, which affects how this function - now actually a coroutine - is called.
Or maybe modify the parameter declaration syntax slightly to make it simpler and more clearly special, removing the ability to externally name the 'yield' parameter since a caller can't address it explicitly anyway (and incidentally enforcing its name within the function, for consistency across all code):
func modify(@yield: (inout T) throws -> ()) rethrows {
do {
try yield(&_value)
}
}
So in a slightly more complicated generator example:
func subscript(_ slice: Range<Int>, @yield: (T) throws -> ()) rethrows {
for i in slice {
do {
try yield(self._values[i])
}
}
}
…
for value in myAboveType[3..<7] {
// Process process process…
// Oh noes!
throw Error.Bewm
}
It's also a little more familiar and intuitive, as a result, as to what order defer blocks are handled in, if they're used amongst this - since now you're pretty explicitly making a call via the yield, at the site of the yield you would expect the callee's defers to run first, then return to you, and then you fall back further up the stack.
One up the stack happens to be the superset function of the one you called via yield, though, so the question would have to be answered: is it possible to have only some of the defers & catches in said function run first, and the rest after the coroutine's?
More concerningly, where does the try go on the call to the coroutine? The only place it could be put is on the call as normal, but the problem is that the actual exception might not be raised until code after that coroutine call has executed…
func subscript(_ slice: Range<Int>, @yield: (T) throws -> ()) throws {
guard somePrecondition else {
throw BadUser.NoSoupForYou
}
for i in slice {
do {
try yield(self._values[i])
}
}
guard postCondition else {
throw OhNoes.WhatNow
}
}
…
for value in try myAboveType[3..<7] { // Might throw here, if the precondition fails…
throw Error.Bewm // Might 'rethrow' here, because of my throw…
}
// Or might throw here, if the postcondition fails.
Even if we accept that exceptions can now appear from places beyond just the statement actually containing the try…
One possibility is to require the entire use of the coroutine be within a contiguous do block, and educate users about how coroutines can work regarding exceptions. I think that education would be acceptable, I'm just not sure this restriction would be practical for the compiler to enforce and not unreasonably limit the use of coroutines.
An alternative approach would be to restrict how coroutines may terminate - i.e. that they cannot throw after a yield. That seems pretty limiting, though - now you're really limited in how you can handle problems with caller modifications to the mutable values you might yield - either find some way to handle it gracefully & silently, or crash the entire app - no middle ground.
On the upside, at least this syntax leaves easy room for future functional expansion - i.e. the ability to send values back into the coroutine, or to have a return value distinct from the yielded values - in a way that would be source backwards-compatible.