r/rust • u/ern0plus4 • 23h ago
🧠educational Discovering the language: labeled block - implement early return style control flow w/o sparate function
There are situations when decisions must be made based on many variables, and in some points in evalution process, additional costly operations must be performed in order to decide. These decisions are difficult to implement in the form of a single logical expression, and even if it is possible, it is unreadable and hard to modify.
Usually the best pattern for this is to create a separate function which implements the decision, where we can use early returns: the trivial and easy cases that can be decided based on a simple condition (especially exceptions) are evaluated first, we return with the result as soon as possible, so as we're going foward, we reduce the complexity of the remaining cases, and the last case is often a simple condition.
Sometimes a separate function is not an option, because too many parameters would have to be passed (and returned, but Rust have tuples for it), or it simply just does not feel right to split a single decision into two functions, I think, the single-responsibility principle (SRP) must work this way, too. It's even more true for not too complex but nested cases (certain part of the condition is consist of more sub-conditions).
In C and C++, I usually implement this pattern by creating a do..while(false) loop, from which I break at several points, skipping the rest of the evalutation. I store the result in a variable declared just before the block, usually initialized to the default value.
It may just be my fault that I haven't studied the Rust textbook enough, but I've only found the functionally equivalent syntax for this today, and I am very happy with it, with its flexibility and elegance: labeled block.
Let's see an example!
The decision is about whether close (do_close_old flag) the old time window and/or replace with a new one (do_replace_window flag), also log some info about the decision (replace_reason enum), see example (close to actual code):
let (do_close_old, do_replace_window, replace_reason) = 'switch: {
if just_created {
break 'switch (false, false, Reason::Create);
}
if window.is_retired {
break 'switch (false, true, Reason::Replace);
}
if window.is_expired(timestamp, lifetime_duration) {
break 'switch (true, true, Reason::Timeout);
}
// complicated condition, fake
let compli = if (x && y) | (a && !b);
let cation = if z > w {
countries.find("USA") && languages.find("English")
} else {
false
};
if compli && cation {
break 'switch (true, true, Reason::ComplicationHappened);
}
(false, false, Reason::None)
};
(Please, don't review my code, I know, I know, somehow the result tuple should be replaced with some named thing.)
I think, it's pretty well readable, even if you haven't met with labeled blocks before. Just as me, until today.
AI disclaimer:
- helped in translation (my English is not suitable for publications)
- the pattern was also suggested by AI, I often ask it to refactor short code snippets
9
u/Excession638 23h ago
I'm aware of the syntax, I know what it can do, and I have never used it. I think when I have that much local state, I tend to put it into a struct and create methods on it, even if the external usage pattern is just MyType::new(...).run(). Too much in one function does my head in.
LLMs probably use this because they're applying patterns from other languages, and don't much care about readability.
2
u/ern0plus4 23h ago
The original was a simple
if..elseif..elseif..else.The story is a typical complex-for-first-sight-only situation: there're many exceptions (retired, expired, and some not even mentioned) and a simple decision after the exceptions.
Anyway, I'm struggling for 30 years with "the function is too long, but only because the quantity of simple conditions" cases, and I have no good answer.
2
u/Inner-Asparagus-5703 22h ago edited 22h ago
this exact reason. I think LLMs really struggle to write rust for some reason, so they usually just write c++ or few other langs but in rust syntax.
3
u/Pretty_Jellyfish4921 22h ago
Unrelated, but I also discovered a weird syntax for match arms
match Some(1) {
val @ Some(_) => val,
None => None,
}
Before that I used to do
match Some(1) {
Some(1) => Some(1), // wrap the value again
None => None,
}
1
1
u/Inner-Asparagus-5703 22h ago
what is the reason to not have same block as function? it barely changes anything & I would argue looks better
1
u/ern0plus4 18h ago
In this case one extra function is okay, but if there was a nested condition, it would require to add another function, which is - in my view - going against SRP.
In C, similar cases are the only legal usage of goto (jumping 1. foward 2. out-of-scope), and I like any languages wich have syntax for it (e.g. break <label>).
1
u/Inner-Asparagus-5703 14h ago
still, it's not very good way to do it, there is a reason why goto is massively avoided. Look at other comments with match arms - looks better, easier to write and read, etc. Oh, and i would say it's really main rusty way to do it.
Good luck on your journey, dude.
2
5
u/Solumin 22h ago edited 22h ago
This is the kind of discussion that can be very difficult to have, not to mention a bit frustrating, because there's just too much missing context. Like, why is
do_closetightly coupled todo_replace_window? What sort of logging are we doing that requires passing around an enum like this? How isReason::Nonea useful state to have?Most importantly: why not just do a big
ifstatement? Throw the complicated conditions into helper functions and you get a shorter and cleaner implementation:rust let (do_close, do_replace_window, replace_reason) = if just_created { (false, false, Reason::Create) } else if window.is_retired { (false, true, Reason::Replace) } else if window.is_expired(timeout, lifetime_duration) { (true, true, Reason::Timeout) } else if compli(a, b, x, y) && cation(z, w, "USA", "English") { (true, true, Reason::ComplicationHappened) } else { (false, false, Reason::None) };This
ifstatement and your labeled block are fundamentally the same thing: a sequence of comparisons, stores, and jumps. (Same with yourdo...while(false)construction in C/C++.) I would expect the generated assembly to look identical, modulo compiler support for both patterns.There are probably other ways to write this. It's really hard to say in an abstract case like this --- I mean,
(false, false, Reason::None)just looks wrong to me, because it's repeating the same thing ("Nothing to do") three times. Two of the branches are the same thing, except for theReasonpart.\ Man, I don't know. It just looks off to me. I get the feeling this is GUI code and that tends to be weird in ways that other code isn't, so maybe that's what I'm feeling.