Looking at your example, I've been trying out several of the mentioned ideas, including finally
and do { yield... }
blocks, but something seems not right with all of them. The fact that yield
can terminate the function has a big impact on control flow, which makes it difficult to satisfy all use cases properly. So, I'm thinking: What if we make the control flow explicit?
Making the control flow explicit
By making the control flow explicit, we can make all use cases work naturally, while making it very clear to the developer when they have to think about termination. We can do this using the following:
- Every coroutine gets an implicit variable,
terminate
, which is set totrue
whenever the coroutine should terminate. - Execution always continues normally after a
yield
. The yield will never terminate the coroutine. If something happens during the yield that should terminate the coroutine, like for example athrow
, then theterminate
variable will be set totrue
. - The compiler enforces that
yield
cannot be called ifterminate
may betrue
. - The compiler enforces that you cannot
throw
ifterminate
may betrue
.
The following examples hopefully make clear what this provides:
Modify
// Modify just works.
// Execution will always continue after yield, which makes the code natural:
modify {
var value = getFromStorage()
yield &value
moveBackToStorage(value)
}
Generators
// A generator requires explicit control flow handling.
// The compiler will let you know:
func generate() {
for index in self.indices {
var value = getFromStorage(index)
yield &value // error: yielding while terminate may be true
moveBackToStorage(index, value)
}
}
// The generator can be fixed as follows:
func generate() {
for index in self.indices {
var value = getFromStorage(index)
yield &value
moveBackToStorage(index, value)
if terminate { break }
} // OK: Compiler can prove that yielding never happens while terminate == true
}
Additional clean-up and yields
// Additional code after the loop is no problem, if it follows the rules:
func generate() {
for index in self.indices {
var value = getFromStorage(index)
yield &value
moveBackToStorage(index, value)
if terminate { break }
}
// We can do some additional clean-up here. As long as it doesn't
// throw or yield, because terminate may be true at this point.
someMoreCleanup()
// Do we need another yield? That's fine, as long as
// we check terminate first to keep the compiler happy:
if terminate { return }
var finalValue = something()
yield &finalValue
store(finalValue)
}
Advantages
This approach has the following advantages:
- Being able to explicitly specify the control flow makes all use cases fit naturally.
- Code that should be on the same level of indentation, can be written that way. It never felt right to me that the code preparing the yield and the clean-up code were on different levels.
- The syntax doesn't require you to specify any additional keywords and/or code blocks if you don't need to.
- If you do need to think about coroutine termination, the compiler will tell you.
- To get the behavior of the proposed
yield
, all you need is:if terminate { return }
, directly after theyield
.