Trailing closures look a lot like control statement (for
/while
) bodies, especially for things like SwiftUI's ForEach
where the name strongly implies that it's like a for
loop. The catch is that you can't use break
or continue
in them. So... what if you could?
The current situation creates a kind of inconsistency in the language. How often have you had to actively remind yourself that you can't break
out of a ForEach
or something similar?
So I'm hoping to start a discussion on what it would take to enable that (and, of course, whether we should).
One possibility: an attribute (say, @control
) on the callback parameter. It transforms the closure signature in a way that is transparent to the caller providing the closure, but inside the for
-like function there is either an additional inout
parameter to indicate how the closure exited (return/break/continue), or the return type is converted to a tuple of the original return type and the exit type (example below).
We would also want return
to function as in a normal loop: not only exit the loop, but return from the calling function as well. This would require the loop function to be generic over the return type of the caller.
A minimal example, using the tuple return type approach:
// Transparently added to the return type of the callback block
// T is the return type of the calling function
enum ControlBlockReturnType<T> {
case return(T), continue, break
}
// Return type of a function that has a @control parameter
// T is the return type of the calling function
// U is the return type of the control (loop) function
enum ControlFunctionReturnType<T, U> {
case blockReturned(T), value(U)
}
// block actually returns (Void, ControlBlockReturnType<T>)
// myForLoop actually returns ControlFunctionReturnType<T, Void>
func myForLoop<T>(@control _ block: () -> Void) {
for _ in someRange {
switch block().1 {
case .break: break
case .continue: continue
case .return(let value): return .blockReturned(value)
}
}
return .value(()) // maybe this can be implicit
}
myForLoop {
// return, break, and continue can all be used in here
}
// the above call is actually transformed by the compiler into:
if case let .blockReturned(result) = myForLoop(···) {
return result
}
// ..otherwise the .value result is used as the effective result of myForLoop()
As I wrote this, I realized making return
work is the trickiest part because it requires all that behind-the-scenes stuff to make it work nicely at the call site.
I've tried to think of other viable approaches, but couldn't think of anything that seemed better.
Thoughts?