I am converting a Python script to Rust. This script takes a user input and eventually update the rc.conf file (NetBSD). There are two types of inputs (for my app), service and flags. For example a valid service input would be something like, dbus=YES. I know I can "split" on the equal sign to grab the service value (YES). There are only a few valid service values, ["YES", "TRUE", "ON", "1", "NO", "FALSE", "OFF", "0"]. In Python I would simply do something like:
Okay... I found this, but I have no idea how or why it works:
fn main() {
let service_values = ["YES","NO","TRUE","FALSE","ON","OFF","0","1"] ;
println!("Find value in service_values: {:?}", service_values.iter().find(|&&x| x == "YES"));
}
With your code you are checking whether "YES" is contained in service_values, and I am not sure it is what you expect.
Anyway, if you use cargo clippy you will receive a hint to use .any in order to check if your value is inside service_value. Something like the following:
As you can see, in this case const can be used because all the possible values are known a priori. Here a small sample playground to show you the behaviour.
WOW! Thank you very much. I don't use foul language; however, Rust seems to be able to bring me to the edge. This makes a lot of sense. I really appreciate it.
For those looking for a more scalable solution (i.e., avoiding a linear search), you can use a Set using perfect hash functions, thanks to the ::phf crate:
Another solution for all the noobs (like me) that might search this forum for a similar question. I am open to critics if this isn't really the Rusty way of doing things.
I think this is a pretty Rust-y way of doing things The main improvements I think you could make are:
If you only require read-only access to a string in a function, it's more flexible to take &str instead of String. This also has the nice side effect of allowing you to pass in string literals, as all string literals are also of type &str.
You can combine patterns in a match using |.
fn main() {
// You can pass in a reference to a String...
let service_input = String::from("YES");
println!("{:?}", is_valid_service(&service_input));
// Or a string literal...
println!("{:?}", is_valid_service("FALSE"));
println!("{:?}", is_valid_service("0"));
println!("{:?}", is_valid_service("enable"));
// You can call `to_uppercase` on a string literal too - it returns
// a String, though, so don't forget the & to reference it!
println!("{:?}", is_valid_service(&"yes".to_uppercase()));
}
fn is_valid_service(value: &str) -> bool {
match value {
"YES" | "NO" | "TRUE" | "FALSE" | "ON" | "OFF" | "0" | "1" => true,
_ => false,
}
}
we define a sequence of valid commands (you mention a list, but since mutation of the list does not seem to be required, we can use Python's immutable sequence, i.e., a tuple):
This is called a linear search: the (average) cost of the search evolves linearly with the amount of valid commands. In other words, if we double the amount of valid commands, it takes (on average) twice as long to know whether a given command is valid or not.
Now, given your example, with a sample size of only 8 valid commands, this is completely fine and actually the fastest way.
Using a Set
However, for people having a need similar to yours, but where their amout of "valid answers" is gigantic,
there is a then faster solution: using a Set. This is a collection where its elements are laid out in memory so that this very test (.contains(&element)) is much faster:
when implemented as an Ordered tree, its (axiomatic) cost is logarithmic w.r.t. the amount of valid commands: to double the processing time of a thousand elements the set would need to contain a million elements (vs two thousands in the linear case);
this is almost equivalent to having a sorted sequence to begin with, and using a binary search to test the presence of an element;
when implemented as a HashSet, the cost does not depend on the number of elements, except when hash collisions are involved.
In Python to create a (hash) set all you need to do is use braces instead of brackets or parenthesis:
To fix this issue with collisions, that prevents the cost of a contains(&element) test from truly ignoring the number of elements in the set, there exist perfect hash sets (and maps) based on perfect hash functions. These are only usable when the elements to be put in the set are known beforehand. Which is the case here, hence my suggesting:
Just because it shows off one of the more interesting implications of the way match pattern syntax works, here's another example that's very nice to use when there are a small number of known-in-advance options.
let user_input = ....;
let valid = if let "YES" | "NO" | "ON" | "OFF" = user_input {
true
} else {
false
};
tweak as needed for a fn return, eg:
fn valid (user_input: &str) -> bool {
if let "YES" | "NO" | "ON" | "OFF" = user_input {
return true;
};
// maybe other ways to be valid..
false
};