Hello, I am attempting to write a general-purpose function for accepting user input and validating it like so:
fn prompt_input<T, F>(prompt: &str, error: &str, mut validator: F) -> T
where
F: FnMut(&str) -> Result<T, Box<dyn Error>>,
{
let mut input_str = String::new();
loop {
println!("{prompt}");
if let Err(_) = io::stdin().read_line(&mut input_str) {
continue;
}
match validator(&input_str) {
Ok(value) => break value,
Err(e) => {
println!("{:?} {error}", e);
input_str.clear();
}
}
}
}
I invoke this function in main() to accept a float input as follows:
fn main() {
let value: f64 = prompt_input(
"Enter the temperature value you'd like to convert:",
"Invalid numeric value",
|raw_input| raw_input.trim().parse::<f64>(),
);
println!("You entered {value}");
}
This however doesn't compile. It says:
expected enum Result<_, Box<(dyn std::error::Error + 'static)>>
found enum Result<f64, ParseFloatError>
at the line inside main() beginning with |raw_input|.
Where am I going wrong and how do I get it work? Thanks!
It would be a mistake to transitively convert arbitrary type parameters to trait objects at arbitrary depths inside a compound type. Automatic conversions are in general undesirable. Rust's automatic conversions are therefore always limited to the simplest and direct cases, in fundamental types. I.e., you can convert a &T to a &dyn Trait for example, but not a Result<MyType<Nested<T>>> to a Result<MyType<Nested<dyn Trait>>>.
Also, you might be confusing the "automatic" conversion of errors with the ? operator, which does insert a From::from() call in the Err case.
Unrelated to the problem, but it would be more idiomatic to expect a FromStr bound instead of requiring a redundant closure.
It's also quite weird how you mix inline and stand-alone format arguments in println!().
Pattern matching against Err is superfluous if you only need to check the variant, it's better written as .is_err().
Formatting a user-facing error is better done through Display; the Debug format is usually not suited for end-user consumption.
Finally, you should use eprintln!() for printing error messages to the standard error stream, instead of stdout.
All in all, I'd write this as follows:
fn prompt_input<T>(prompt: &str, error_msg: &str) -> T
where
T: FromStr,
T::Err: Display,
{
let mut input_str = String::new();
let stdin = std::io::stdin();
loop {
input_str.clear();
println!("{prompt}");
if stdin.read_line(&mut input_str).is_err() {
continue;
}
match input_str.trim().parse() {
Ok(value) => break value,
Err(parse_error) => {
eprintln!("{error_msg}: {parse_error}");
}
}
}
}
Thank you for the detailed feedback, much appreciated! I will incorporate all those points. Your version makes the calling of the function much simpler as well.
T: FromStr,
T::Err: Display,
This is quite clever. I didn't realize you could specify a second, deeper trait bound based on the first, shallow-er trait bound. Nice!