Modify Accessors

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 to true 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 a throw, then the terminate variable will be set to true.
  • The compiler enforces that yield cannot be called if terminate may be true.
  • The compiler enforces that you cannot throw if terminate may be true.

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 the yield.
3 Likes