Background: thought of this while reading yaah's post on try blocks, then tweeted about it. Further background: have wished for something like it several times while writing Rust, but it wasn't defined enough until now.
Proposal: introduce a keyword or mechanism (special-cased macro, whatever) to "early exit" at the block level.
This is in symmetry to:
-
return
at the function level -
break
at the loop level
Some prelims:
- I have used the imaginary keyword
breakout
for this. This is not intended for serious consideration, don't bikeshed it - The label syntax is also probably bikesheddable but I'd like to focus discussion on the concept rather than syntax at first.
Trivial example:
let custom = 'up: {
let c = get_config();
if c.all_defaults() { breakout 'up None }
// more processing
if c.invalid_condition() { breakout 'up None }
// even more
Some(c)
};
This could be written today as:
let config = {
let c = get_raw_config();
if c.all_defaults() {
None
} else {
// more processing
if c.invalid_condition() {
None
} else {
// even more
Some(c)
}
}
};
or even:
let custom = loop {
let c = get_config();
if c.all_defaults() { break None; }
// more processing
if c.invalid_condition() { break None; }
// even more
break Some(c);
};
So at first glance, this is a generalisation of the pattern when you don't actually want a loop, saving some syntax / making it more clear. However, notice I used a label on the breakout
example and not on the loop
example: that's because all blocks can be broken out of:
if let Magical(child) = candidate 'schooling: {
if child.is_squib() { breakout 'schooling }
child.parents().inform();
let letter = Hogwarts::introduce(magician);
Hogwarts::owlery().send(letter)?;
if child.parents.request_visit {
let prof = Hogwarts::staff().travel_to(child.residence);
if child.parents.refuse_offer {
prof.obliviate(child);
prof.obliviate(child.parents);
breakout 'schooling;
}
}
child.go_to::<Alley<Diagon>>().shop(letter.supplies_list())?;
}
This in turns desugars to:
if let Magical(child) = candidate {
if !child.is_squib() {
child.parents().inform();
let letter = Hogwarts::introduce(magician);
Hogwarts::owlery().send(letter)?;
if child.parents.request_visit {
let prof = Hogwarts::staff().travel_to(child.residence);
if child.parents.refuse_offer {
prof.obliviate(child);
prof.obliviate(child.parents);
} else {
child.go_to::<Alley<Diagon>>().shop(letter.supplies_list())?;
}
} else {
child.go_to::<Alley<Diagon>>().shop(letter.supplies_list())?;
}
}
}
Alternatively, this could be broken into a function, and early return
used, but that has implications for variable scope and type inference etc.
Desugaring to clean Rust gets even more complex with multiple labelled "bare" blocks, but I'll leave that as an exercise to the reader and talk about more block types:
Unsafe: pretty much identical as bare blocks above, except the innards are unsafe.
Async: this is, I think, a no-go. Happy to be wrong, though. If wrong, it would look like:
async 'conf: {
let config = match fs::read_to_string().await {
Err(e) => { eprintln!("no config: {:?}", e); breakout 'conf; }
Ok(c) => c
};
let config: Config = config.parse()?;
// etc
}
Try: this is interesting, especially in the context of debating throw
/raise
/yeet
keyword syntax, as:
try {
yeet some_error;
}
would become sugar for:
try {
breakout Err(some_error);
}
and this proposal would also allow early-ok-exit:
try 'trying: {
// stuff
if good { breakout 'trying Ok(success); }
// more stuff
breakout Err(fail);
// etc
}
which with only try-blocks, would desugar to:
try {
// stuff
if good {
success
} else {
// more stuff
yeet fail;
// etc
}
}
Edit: this already exists, but is stalled: https://github.com/rust-lang/rust/issues/48594
Afaik the async interaction hasn't been explored, though.