Summary: add fn read_input<T: FromStr>(message: &str) -> T;
to allow programming beginners to very easily read a value from stdin (and prompt the user beforehand). This would automatically deal with all errors and difficulties.
Movitation
Over the last couple of years, I worked a lot with programming beginners, i.e. people just learning how to program. I also held a lecture on Rust. Based on these experiences, I would really like to see a particular feature in Rust's standard library: easily reading values (of some primitive type) from stdin
.
When starting to learn how to program, stdin
is a main way of user interaction. Many programming tasks in many introductory courses use this kind of user interaction to make the tasks a bit more interesting (e.g. "Please guess a number: "). After all, most people are used to "interactive" programs and programs without interaction could be alienating. (The same could be said about the use of the command line, but that's a different topic; the command line is still widely used in introductory courses.)
So how to read from stdin
in Rust? When googling, one can find this StackOverflow answer suggesting a 9 lines solution which also introduces two temporary mutable variables. Other search results show similarly long solutions.
Of course, many results also suggest extern crates such as text_io
, scan-rules
or whiteread
. These are surely nice and useful crates, but they are extern crates. And that might pose a problem as an introductory course will probably not explain dependencies and this stuff early on (maybe this concept is even introduced fairly late). Sure, one could just tell the students to add a line to Cargo.toml
(if one is even using Cargo) without explaining it, but that's not optimal. So this is a legitimate problem I think!
That's why I would like to add a super simple, "best-effort" function to std
to read from stdin
.
It is worth emphasizing that this API is only intended for programming beginners and quick scripts. I don't expect it to be used in any production application/library. This is similar to dbg!
. Almost everyone liked this macro, although it will probably never end up in production code (on purpose).
Finally, note that one of the first chapters of the Rust book has to deal with exactly this problem. I don't know if this was (at least in part) intentional in order to teach this stuff early on, but in my opinion it's overwhelming for programming beginners and should not be explained that early. So in my opinion, the Rust book could benefit from my "proposed" addition to std
.
The exact API
Basically, the programmer has to deal with these things:
- IO errors (stream is closed, ...).
- The input could be invalid for the type we want to read.
- If the user should be able to write the input on the same line as the prompt,
stdout
needs to be flushed explicitly. -
read_line
includes the line break, meaning atrim
is advisable.
I'd like to offer an API that frees the user from handling all of this stuff. For example, the last two points can easily been dealt with automatically.
I would solve problem of invalid inputs by printing the error and asking the user to try again. This sounds really strange as basically no other API in std
prints anything (except if that's their main purpose). But by "best-effort" above I meant that the function shouldn't strive to be useful in all situation, but be "good enough" and very simple for >90% of situations.
It's still unclear to me what's the best way to deal with IO errors. Of course in good Rust code, we would want to pass the error up the call stack until a function can deal with it or notifies the user. But this error handling is something that a programming beginner probably shouldn't be dealing with at the very beginning: learning to program is hard enough already! It should be noted that IO errors in this situation are especially rare, meaning that the beginner would write could that they would probably never see executed. Therefore I would tend to panic or abort when an IO error is encountered.
With that, I would think of an API like this (names are still up for debate, ignore them for now):
fn read_input<T: FromStr>(message: &str) -> T;
This would probably live in std::io
(or can anyone think of a better place? Or new module?). The function returns T
and not Result<T>
or Option<T>
, as all errors are dealt with internally. Using the function like read_input::<u32>("Please enter your age")
would result in an output like this (with peter
and 25
entered by me in the terminal):
Please enter your age (value of type `u32`): peter
-> Error: the input "peter" is not a valid value of type `u32`: invalid digit found in string. Please try again.
Please enter your age (value of type `u32`): 25
Of course, the exact output is unspecified. Here is an example implementation:
#![feature(specialization)]
use std::{
any::type_name,
fmt::Display,
io::{self, BufRead, Write},
str::FromStr,
};
pub fn read_input<T: FromStr>(message: &str) -> T {
let mut input = Vec::new();
let stdin = io::stdin();
let mut lock = stdin.lock();
loop {
// Prompt the user to enter a value:
print!("{} (value of type `{}`): ", message, type_name::<T>());
io::stdout().flush()
.expect("An unexpected IO error occured when trying to flush stdout");
// Read bytes until '\n'
input.clear();
lock.read_until(b'\n', &mut input)
.expect("An unexpected IO error occured when reading input from the user");
// Try to obtain a `&str` by checking for valid UTF-8
let s = match std::str::from_utf8(&input) {
Ok(s) => s.trim(),
Err(e) => {
eprintln!("Error: the input is not valid UTF-8 ({})! Please try again.", e);
continue;
}
};
/// This is just a helper trait to check if `T` implements `Display`.
/// If so, the `details` method returns a string with additional
/// information, otherwise it returns an empty string.
trait ErrorDetails {
fn details(&self) -> String {
"".into()
}
}
impl<T> ErrorDetails for T {}
impl<T: Display> ErrorDetails for T {
fn details(&self) -> String {
format!(": {}", self)
}
}
match s.parse() {
Ok(value) => return value,
Err(e) => {
eprintln!(
" -> Error: the input \"{}\" is not a valid value of type `{}`{}. Please try again.",
s,
type_name::<T>(),
e.details(),
);
continue;
}
}
}
}
Additionally, one could add more functions, like:
-
fn read_inputs<T>(message: &str) -> Vec<T>
: reads multiple values that are separated by whitespace -
fn try_read_input<T>(message: &str) -> Option<T>
: whenFromStr
returns an error, the message is printed butNone
is returned instead of asking again. - ... there are a couple of possible extensions.
Discussion
My main question is: what do you think about adding a function that is completely targetted at programming beginners and for writing quick scripts? Personally, I think it's fine and very comparable to dbg!
, but I'd like to know your opinions.
There are plenty of other things to discuss, like how to deal with IO errors, whether to add a read_inputs
, too, or if read_value
is a better name. But these are secondary questions right now.