Add plain-simple way (in the standard library) for reading values from `stdin`?

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 a trim 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>: when FromStr returns an error, the message is printed but None 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.

5 Likes

I think this would be better served by a crate, you can just tell them that this crate has a lot of nice things that make things easier for beginners but wouldn't be suitable for std. You can then put in lots of helpful, but otherwise unnecessary or very simplified versions of apis that beginners would find useful. For example your read_input and friends.

std is meant to have things required for the compiler, like Iterator or UnsafeCell, vocabulary types like String, Vec<T>, and Option<T>, or primitive os bindings like the net and sync module. These sorts of nice to haves are why we have cargo, so we can easily pull in dependencies.

13 Likes

@RustyYato beat me to it; a single crate for beginners makes more sense to me and will be extensible. std has to contain all those things that require compiler magic, such as invoking LLVM intrinsics or notating special LLVM attributes (e.g, for UnsafeCell).

Unfortunately, for historical reasons std also contains some things that do not fall in that category, and we've already seen that some of those should be replaced (but can't because they are in std).

3 Likes

So yeah, of course a crate seems like the right choice here in normal circumstances. But this is not a normal circumstance as we specifically deal with programming beginners. In order for them to use an external crate, there are only two possibilities:

  • The teacher teaches about extern crates, Cargo and stuff at least roughly. Students would then approximately know what's going on. I think it's not beneficial to teach this stuff early on.
  • The students are told to just copy some code to import that crate. That is of course possible, but many students I talked to disliked the "ignore this for now" magic at the beginning. Sure, some won't mind but I think it's beneficial to reduce (as much as possible) the number of things the student has to do without understanding.

So, in my opinion, neither option is good. Both have serious disadvantages and I wouldn't be happy to use either in my lecture. Additionally, extern crates basically require Cargo -- but the teacher also might not want to use Cargo from the very beginning.

What are your comments about that problem?


Again, I can understand your hesitation to put something like this in std. That's because the reasons for inclusions are different than for almost all other things in std.

Your views of what should be in std sound very restricting though. What about dbg!? In your opinion, was it wrong to include that macro in std?

4 Likes

If you can get the crate added to https://play.rust-lang.org/ then it basically mitigates all of those concerns (except the magic).

That said, it's pretty rough to do rust without cargo. Also, with the 2018 edition, all you have to do is add a single line to the Cargo.toml file, no extern crate.

4 Likes

Dbg is a good example of a class of items that do probably belong in std, "nontrivial CS 101 items I would write myself before hauling in a dbg crate". I could see a high quality educational I/O shortcut crate being included (partially or fully) in std in the future, to enable teaching rust without so much of a boiler plate or skeleton. I could also see it being useful for simple rust scripting that doesn't particularly need to be fast (making it fast would be nice too).

1 Like

This isn't my views, this is what I think the views of the Rust team is. Gathered from posts here on internals and on the Rust issues and PRs. Basically, crates.io is supposed to be the batteries you would usually find in std of other languages.

dbg is so small that no one would use it if it was in another crate, and it is used by every level of Rust developers.

The functions that you describe will only be used be Rust beginners, as they are simple functions that make a lot of decisions for you, for example, allocating on every call. These decisions are likely never what you want for production-grade software. Also, they lack flexibility in favor of simplicity, meaning that if you are using another crate, (which you likely will be for a real project) there is no need to have these functions. Thus these functions are unfit for std.

You don't have to give an in-depth explanation of how cargo works. You can say

cargo finds crates on the crates.io website and downloads and connects them to your code for you.

No magic, but also not a huge in-depth explanation of how things work. As @samsieber said it's only a single line in Cargo.toml with the 2018 edition

4 Likes

Could you say some more about why you think teaching about external crates is not appropriate to introduce early on? In my own personal view, I think external crates are fundamental to the rust language and ecosystem, and so they deserve early attention.

I sympathize with you here. The docs for the std::io::stdin() method say this:

Constructs a new handle to the standard input of the current process. Each handle returned is a reference to a shared global buffer whose access is synchronized via a mutex.

For someone who has never programmed before, that's a lot of unpack. References, shared state, global state, synchronization, mutexes! That's a whole lot of new and complicated topics to introduce just to be able to fully understand the does for stdin. You might have to introduce the concept of stdio streams or file descriptors, too!

I'm personally weakly opposed to a new read_input function, but I don't have a good answer for you. Whenever you teach programming to beginners, I think there will always have to be topics that you paper over with "run this magic code for now, and we'll come back to it later". Maybe this is one of those topics when teaching rust.

(In reading the StackOverflow answer you linked -- it might actually be a good teaching example. It's small enough to go through it line by line, and it involves some basic topics that are good to cover early -- mutable variables, references, the match function, etc).

Thanks for your efforts in teaching to beginners, especially teaching them rust. Good luck!

3 Likes

Doesn’t the simplest toy Rust program already involve a Cargo.toml and a few cargo commands to set up?

You could also simply have a git repo with a toy program that already has several dependencies (a lot more than just read_input), so the novice only has to do git clone and cargo build to get started, with no discussion of dependencies.

While I sympathise with the use case, I think this is an XY problem, and adding pedagogical tools to std would only be treating the symptom. Any tweak you want to make to your introductory course shouldn’t require convincing us to change the standard library for everyone; it should just be a tweak to whatever code/template/repo/script you provide for your course.

1 Like

Thank you all for your comments!

If someone wants to add stdin interaction support to the Rust Playground, I would indeed be pretty happy :smiley:

To be clear: I'm mainly talking about programming beginners. People who start learning programming with Rust. I don't think this has happened a lot yet, but it's still worth talking about it.

And for programming beginners, the important part is to get used to the "new way of thinking". And external crates (like many other features) are not required for that. Of course, for people who already know another language, teaching about Cargo and crates early is important!

To be clear, my work with programming beginners and the work with teaching Rust were two independent things. The former was with Java. I actually did not teach Rust as a first language yet.


Reading your comments and thinking about it again, I have to agree that the "just add this one line to your Cargo.toml" is not that bad. To be honest, suggesting something like this on IRLO was on my TODO list for two years already and I today decided to tackle that. With Rust 2018, it's indeed only one line and should be fine. Additionally, I remembered that there are tools like cargo-generate which could make a few things a lot easier.

To be clear, I didn't give up on this idea completely yet: I still wouldn't be opposed to including something like this in the standard library. But you convinced me that the alternative isn't that bad.

4 Likes

If there was a beginners learning crate, I bet we could get it added to the rust playground: https://github.com/integer32llc/rust-playground

There is the issue that you need to use "extern name_of_external_crate", but you could easily load that up in a template and share the link to let people use it.

Barring that, it should be possible to self host the rust playground.

The playground doesn't require extern crate in rust2018 mode. All top 100 crates (+ dependencies (?)) are just available to be used.

Ah, awesome. The docs weren't clear on that part, but I'm happy for thats how it works.

:heavy_plus_sign::100:

Reading text input with only std facilities is a pain at the moment, and this is important in at least three contexts:

  • teaching rust to complete beginners
  • solving algorithmic challenges at various code platforms (some, but not all, of such platforms include a set of blessed crates, with an IO helper, and for those that do this, it's an administrative overhead: you need to update both the language and the crates).
  • quick and dirtly exploratory programming (for example, if, when debugging a specific bug, you want a repl in the specific part of your program).

However, these use-cases are somewhat different, so I would try to look beyond just the first one. So, I'd imagine a text_io module which includes some building blocks for "reading space-separated values from utf-8 Read" plus a number of convenience functions, like python's input, build on top.

Personal anecdata: when I was learning programming, I was hit by 1 & 2 simultaneously, as I was using Java for doing algorithms. Figuring out the required nesting of StreamReader, BufferedReader, Scanner and StreamTokenizer was a pain. C++, with its std::cin >> value is much more beginner friendly in this respect.

10 Likes

FWIW, I also strongly support adding something like this. I think it should probably leave the FromStr::parse() call to the caller (should be easy enough to tack on, you won't always need it, and for learning purposes it's probably better to keep it orthogonal) and return a Result<String, io::Error> so that any error handling can be left to the caller as well.

1 Like

What are some examples of this?

I think it would be bad practice to, by default, condition beginners into ignoring errors. It would be a tough bad habit to unlearn.

As a former teacher of university programming lab sessions myself, I'd also like to point out that in beginners' programming exercises, it often is the point (or at least a part) of the task to parse the input.

Nonetheless, once the exercises level up from there, having to perform the same integer parsing over and over again can become annoying. So I'd welcome a function that provided similar functionality if it didn't ignore error handling.

I'm still not sure if std needs to contain it, though. Maybe a rand- or serde-like "blessed crate" would do the job.

Actually, many teachers of programming opt to write a sort of utility library specifically for introductory teaching purposes and they allow students to use it along with the standard library. This has the advantage that said library can be tailored to the specific methodology the teacher intends to follow.

2 Likes

That's a very good point. In my example API/implementation, my hope was that programming beginners don't even notice IO errors can happen and thus are not conditioned to ignore them. But I would agree the, for example, telling the beginners to write unwrap() in their code to deal with the error without thinking about it.

That's also what was done in the introductory courses I was part of. The advantage of being able to tailor the helper library to the programming course is huge, of course. It also is an argument against including such a super-beginner function in the standard library, as most courses would use their own function anyway.


But there seem to be some people who would like to see more features in std to make reading from stdin easier. Not something "super simple for beginners" as I proposed, but a proper API that is a bit harder to use, but more flexible.

For this, we should probably take a look at the three crates I mentioned in the first post. With this new perspective on things, I might try to come up with an initial design for an API that would fit std in the coming days.

Thanks again everyone for your insightful comments!

1 Like

I feel like IO is the least of Rust's problems if you don't have crates. Calling .next().unwrap().parse() on a SplitWhitespace isn't beautiful, but it's way less of a just-type-it-out problem for challenges than not having regex::Regex or not having num::BigInt.

That said, maybe there's a place for something like fs::read_to_string for Stdin, or some kind of .tokens() like there's .lines() (though that's arguably better called unicode_segmentation...)

1 Like

Hm, I've never seen a "classroom" algorithm's challenge that requires a regex (or other fancy crates). BigInt is definitely is required for some tasks, but reading the input is required for all tasks. That is, I agree that inventing your own input is much simpler than inventing your own big int, but, if we weight it by the relative frequency, I still personally find the input to be the bigger problem.

1 Like