In rust I sometimes need to write chained if-else statements. They are not the nicest way to manage multiple conditionals with multiple things to check in the conditions.
Here is an artificial rust-playgroung example of what I mean and in the following code you can see the if-else chain in question.
// see playground for rest of the code
fn check_thing(t: Thing) -> CarryRule {
let allowed = vec!["birds","dogs","cats","elefants","unknown","veggies","meat"];
let max_carry_size = 30;
let max_carry_unknown_size = 3;
let max_carry_dog_size = 5;
let typ = &t.typ.as_str();
if t.size > max_carry_size {
CarryRule::Forbidden
} else if ! allowed.contains(typ) {
CarryRule::Forbidden
} else if t.typ == "unknown" && t.size > max_carry_unknown_size {
CarryRule::Forbidden
} else if t.typ == "dogs" && t.size > max_carry_dog_size {
CarryRule::Forbidden
} else if t.typ == "birds" {
CarryRule::UseCage
} else {
CarryRule::UseBox
}
}
I know, I should use a match statement, but I do not see how I can do all of the checks above using a single match block. I would need to
- match the
t.typ - check if the
t.sizeis greater than some value - call the
allowed.contains(typ)function
I am looking for Rust version of Go's non-parametrized switch-case such as the following.
switch {
case a && b: return 1
case c || d: fallthrough
case e || f: return 2
default: return 0
}
Of course, I could also refactor the whole example, modelling t.size, t.typ, and the allowed list in a more consistent way that allows nicer match blocks. But sometimes these types are outside of my control and I do not want to wrap the given types in too much extra wrapping.
What are good readable alternatives to such if-else chains with complex conditions in Rust?
Based on the good recommendations in the other answers, here is a playground with my preferred solution, which uses the following new
check_thingfunction.This solution adopts the proposed enums and match guards but also focusses on readability. It tries to keep all if-else logic in the
check_thingfunction, where it was before. And as before, it applies this logic step by step as it was in the if-else chain.But let's come back to the original question:
Here are some options that can help and that can be used complementary.
Match & Guard
Use a simple
matchstatement (one argument only) and implement some of the complexity as match guards. Also, do not use strings for matching, but anenum. This way the rust compiler can help you covering all the logic.A longer
matchwith a single argument and additional guards can stay quite readable and is the closest you can get if you are looking for something like Go's plainswitch.Split Up
Split up the if-else blocks in separate parts and use early returns instead of adding more else blocks (see the generic and specific parts in the example). If your match guards are too complex find the generic parts and move them up.
Generalize
Express your complex conditions as your object's configuration rather than in long if-else chains. Using a
traitor simply adding anenumcan help to capture some of the logic and make your entities more configurable.This kind of refactoring and generalization requires more code changes if you start out with a big complex if-else chain. But it can make your logic configurable and extensible, and may often be the preferred solution if you are the owner of the code base anyway.
Here is this generalized version.
As you can see, it separates the
Typandsizelogic. This has the benefit of making things configurable and reusable, but also has the drawback that the carry logic is now in different places. You can no longer read the rules from top to bottom as in the original if-else block or in my preferred solution at the beginning of the post.It is up your project requirements which solution you should take.
Finally, thank you for all comments and answers, and for teaching me some more Rust today!