In fact, "non-local control flow" could be implemented as syntax sugar around this pattern. For example, if the caller wrote something like this (based on the OP's example):
s.iter().try_fold(0, |acc, &x| -> i32 {
if x == 0 {
return 0;
} else if x == 1 {
break;
} else {
acc * x
}
})
the compiler would automatically transform it to:
// (the compiler would make a new enum for each invocation,
// opaque to the programmer)
enum Nonlocal123 { Return(i32), Break }
match (s.iter().try_fold(0, |acc, &x| -> Result<i32, Nonlocal123> {
if x == 0 {
Err(Nonlocal123::Return(0))
} else {
Ok(acc * x)
}
})) {
Err(Nonlocal123::Return(val)) => return val,
Err(Nonlocal123::Break) => break,
Ok(x) => val,
}
From the callee's perspective, there would be no magic, none of the silent nonlocal control flow that we try to avoid (cf. exceptions). And this would be highly compatible with the existing ecosystem. Unsafe code wouldn't have to be updated to consider a new type of silent control flow in addition to panics. In fact, APIs wouldn't have to be updated at all to support the new kind of closure; you could just use existing Result
-based methods like try_fold
. (In cases where try
_ variants don't exist, like this one I ran into recently, it would be a good idea to add them regardless of whether any new language features are added.)
But yes, there would need to be some way for the caller to distinguish this new kind of closure from the existing kind.
One possible approach is based on observing that this isn't just syntax sugar for a closure; it's syntax sugar for a function call where (at least) one of the arguments is a closure literal. The syntax has to be aware of the function call, because that's where the match
gets inserted; the whole scheme depends on the called function passing on Err
s that it encounters.
So we might as well go the whole way and add the kind of syntax sugar that @withoutboats mentioned exists in other languages, and have the auto-Result-wrapping happen iff the sugar is used.
However, fitting that kind of sugar into Rust's syntax is a bit tricky. It would be nice to be able to write
foo() |x| {
x + 1
}
but this is already legal syntax that does something else: the pipes are interpreted as bitwise-OR.
This syntax looks clean and isn't already taken:
foo() {
// ...
}
...but if the closure has arguments, where would you write them? You could do it Ruby-or-Swift style by putting the arguments after the opening brace, but that would be weirdly inconsistent with existing closures.
My favored approach is to just use a keyword, reminiscent of Ruby:
foo() do |x| {
x + 1
}
This would also give us a name: we could call the new feature "do
closures".
In addition... while this kind of syntax sugar only makes sense when passing a single closure, the Result-wrapping scheme could potentially support passing multiple closures to the same function call. If we wanted to support that, we could reuse the same keyword with a more traditional syntax:
foo(do |x| 123, do |x| 456);