Why is it so difficult to get user input in Rust?

We can simply read user input in C++ or Python

Ex (for C++):

std::cin >> name;

Ex (for Python):

name = input("Your name: ")

But, in Rust we have to use that "things"

use std::io;
use std::io::*;

fn main() {

    let mut input = String::new();
    io::stdin::().read_line(&mut input).expect(“error: unable to read user input”);
    println!("{}",input);

}

all that things for read a simple input. Why isn't it made simpler?

7 Likes

One way to look at it is that Rust doesn't focus on small programs with simple problems. It handles problems that arise in bigger, more complex programs.

For example, instead of String::new() you have an option of using String::with_capacity(…) to pre-allocate the expected size of the buffer, for efficiency. It also forces you to decide what to do when reading from stdin fails (it could be unavailable or closed).

6 Likes

Your C++ example is far from complete. The equivalent Rust code would be

std::io::stdin().read_line(&mut name)?;

Which, compared to std::cin >> name; is not that bad.


Now, more generally, this is where low level language vs high level language comes into play:
a function like Python3's input function can be written in Rust, but these kind of functions are not shipped in the ::std lib, since there is no single implementation to rule them all. There are tradeoffs between performance, error handling and ergonomics that can lead to multiple APIs.

Example: basic input() function

use ::std::*;

/// # Performance
/// Allocates a new `String` every time it is called.
///
/// # Panic
/// Panics if it fails to read from `Stdin`
fn input (message: &'_ impl fmt::Display) -> String
{
    print!("{}", message);
    let mut ret = String::new();
    io::stdin::().read_line(&mut ret).expect("Failed to read from stdin");
    ret
}
10 Likes

The full C++ code, which would do the same as the Rust example, would be:

#include <iostream>
#include <string>

int main()
{
    std::string name {};
    std::cin >> name;

    std::cout << name << '\n';

    return 0;
}

Now, I don't see how Rust is so bad. You can write a Rust function to return the input, so you can have a one-liner in most of your code, but the same goes for C++: by default, it is not one line, but you can abstract it into one line if you want.

That said, where I would like to have a shorter equivalent in Rust, is when reading other types than Strings into variables from stdin. In C++:

#include <iostream>

int main()
{
    int x {};
    std::cin >> x;
    // and it just works! I input an integer without worrying to convert from a string.
    std::cout << "You entered: " << x << '\n';
    return 0;
}

Rust:

use std::io;

fn main() {
    let mut x = String::with_capacity(5);
    io::stdin().read_line(&mut x).expect("Error reading input");
    let x = x.trim().parse().expect("Error parsing number");
}

I would really appreciate if we could get overloaded read_line() methods on io::stdin() to make that trim().parse() stuff obsolete.

1 Like

It's suprising how little I find need to get user input. For me, it's almost always easier to have some sort of config format e.g. toml and then use serde to read the data in.

Having said that I did make some functions to get data of the user. I'm on the wrong computer or else I'd post them. They used FromStr to read in lots of different types, and automatically looped and asked for another line if they couldn't parse what was typed. They also allowed for default options etc. I'll try and turn it into a crate maybe, it would be especially useful for people just starting who want to get data from stdin easily.

7 Likes

I think one of the things that create friction for beginners and boilerplate in rust is actually one of the strong points of rust.

Yes, C++ will let your write fallible code without having to handle the potential errors. In rust you can't ignore their existence. It also means you don't have to figure out for yourself all the things that can go wrong. If the method returns a result, you know it can fail.

It's also not the same like say java where literally everything can throw exceptions. In the long run, if you want to make reliable software, the boilerplate of rust is more than worth it.

As for python, I choose not to write python where I can avoid it, but I have seen many more python backtraces in my life than I would have liked as an end user. Looks like a language which hides errors from the developer shows them to the end user instead.

20 Likes

First off, I agree with you that C++'s std::cin can hide the fact that it failed from you. You don't have to handle a potential error for your code to compile. This is something where I agree that Rust is better.

However, you can feed std::cin a lot of different data types. In Rust, getting integer or floating point numerical input from the console is an ugly three-step process:

  1. Declare a mutable String
  2. Call std::io::stdio().read_line() with a & mut borrow of your String declared in step 1.
  3. Declare a new variable of the actual type you want and init it with the result of the parse() method on the String.

In C++, you can combine 2. and 3. above with std::cin >> x; where x is an integer.

I want to have a syntax where you can do

use std::io;

fn main {
    let mut x: i32;
    io::stdio().read_line(&mut x).expect("Failed to parse int. Please only enter digits.");
}

But I also see that this opens a whole new can of worms in terms of mutability, as x is now mut for the rest of the function, and to shadow it with another x would just use 3 lines again :confounded:

Anyway, coming from C++, this is the one thing I find extremely stupid: that you have to allocate and grow a String to get an i32 instead of directly using the input buffer. Maybe I am just a noob and don't understand that there is no other way to get a number from keyboard (character) input :slight_smile:. Maybe you clever people can come up with a solution.

2 Likes

I rarely use stdin, so I'm not the best placed here, but in any case it implements std::io::Read, which is more generic than just read_line. You can read into a &mut [u8], which means you can then convert the contents of that into your type. Unfortunately, I think creating integers from a byte buffer takes more than 3 lines in rust atm. It actually takes a dependency on the byteorder crate, but if I'm not mistaking after that you should be able to use the ReadBytesExt trait to do something like:

let x = io::stdio().read_i32::<LittleEndian>().expect("Failed to parse int. Please only enter digits.");

Actually, I'm not quite sure how that will handle newline...
You probably need to know the amount of digits of your number to figure out how many bytes it will be... And surely that's why in the end treating it like a string with a newline on the end is not all that bad.

I am not sure how all that stuff works behind the scenes. I just know that I have this minor nitpick with Rust that I have to do this:

use std::io;
let mut x = String::new();
io::stdin().read_line(&mut x).expect("Failed to get console input");
let x = x.trim().parse<i32>::().expect("Failed to parse int");

instead of:

#include <iostream>
int x {};
std::cin >> x;

But, to get console input, then further treat x as immutable in C++, you need:

#include <iostream>
int x {};
std::cin >> x;
const int y { x };

And to handle the error case correctly, you need:

#include <iostream>
int x {};
while (true)
{
    std::cin >> x;
    if (std::cin.fail())
    {
        std::cin.clear();
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
        std::cerr << "Failed to parse int";
    }
    else
    {
        break;
    }
}
const int y { x };

So Rust isn't too bad after all :wink:

4 Likes

No, .read_i32::<LE>() reads 4 bytes from stdin, transmute() it to i32, and .from_le() on it. It does not care about ascii integer at all.

1 Like

I think comparing the error handling cases is the right thing to do. Because in an interactive program, ignoring failures (which cin >> does by default) is always the wrong thing. Okay, it's fine in toy programs, maybe, but for anything even remotely serious? You can't just turn anything that isn't a digit into 0 and silently continue. Worse, if the user happens to accidentally type something like 1- instead of 10, like I did just now while testing, the - stays in the input stream to screw with any input you might want to get in the future. Except when it's a whitespace character; those just vanish, because it's 1985 and everybody's writing space-separated formats.

cin >> is optimized to make the worst behavior (turning decoding errors into junk data) the easiest thing to do. And it has this extra thing where it does arbitrary and inconsistent things with leading or trailing characters. In fact, I think it's impossible to use cin responsibly for interactive programs because its behavior with respect to whitespace is just too weird; you should always use readline instead. This not only makes your interactive program act more or less like every other interactive program, but also makes it vastly easier to print useful error messages like '1-' is not an integer.

Now, if you're writing a program that takes structured input (CSV, JSON, etc.) and you're parsing it? Maybe you have a case for using >>, but you still need to check the failure state after every decoding that can fail, because otherwise you'll treat 10.1 as two or even three tokens instead of one, and that will trash your whole state machine. So you better be really religious with setting the delimiters appropriately and checking the flags after every operation. You'll probably be better off writing your own lexer and processing a stream of tagged tokens instead of raw text, unless, of course, you can just plug in an already-written CSV or JSON parser which bypasses the whole problem.

And if you take the above very sensible advice and apply it to C++, hey, it's actually just as easy or slightly easier to do all this stuff correctly in Rust. It's only the wrong stuff, the things you should almost never do, that's easier in C++.

Python, on the other hand, is a whole different story. Because it actually does the right thing: input returns a string, and when you try to convert one to an int, it raises a ValueError containing the problematic string. Python fails hard and it fails fast. Which is just exactly what you want a lot of the time, for prototypes and one-offs -- "bail out" is the most conservative thing you can do, and if there's some other way to handle it, you can always catch the error and continue.

This post is getting way longer than I wanted it to be. I was going to also say something about how Python's approach is fine and Rust's approach is also fine, but they serve different purposes. However, all I really want to point out with this post is that you have the right idea when you render the error-checking version in C++, because unless you're writing toy, fizzbuzz-esque programs, you absolutely must have something beyond cin >> x.

31 Likes

Well, this still raises the question: why the need to allocate a String first? Can't you have all the error checking and all the other niceties of Rust, but operate directly from stdin to your i32 or bool? Doesn't it slow the program to have to allocate a second, unnecessary, intermediate buffer just to read from stdin to something else than a String? What is happening under the hood?

1 Like

Reading from stdin in a sense is not different from reading from files. (On Linux both stdin and files accessed via file descriptors and same syscalls) So for user-land application stdin is a simply stream of bytes which it gets from OS, thus for converting ASCII integer it has to use a temporary buffer to store those bytes (though it can be stack-allocated, but it will be somewhat convenient to use).

4 Likes

Right I made my code into a library to do this, called read-human. Hope it's useful. :slight_smile:

EDIT wrong name duh

2 Likes

Minor detail:

  • the line
1 Like

If you want cleaner code for reading lines, you could do something like the code below. The read_line function here is generic, so it returns the type you want.

use std::io;
use std::str::FromStr;
use std::error::Error;

fn read_line<T: FromStr>() -> Result<T, Box<Error>>
where <T as FromStr>::Err: Error + 'static
{
    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    Ok(input.trim().parse()?)
}

fn main() -> Result<(), Box<Error>> {
    let u: u32 = read_line()?;
    let v: String = read_line()?;
    println!("{}", u);
    Ok(())
}
13 Likes

I vote for this function to be included in some form in std! This is exactly what I think the OP (and I) wanted.

I also made a crate that is helpful when getting user input read_input.

Maybe we should compare notes.

4 Likes

Awesome, I didn't find this when I looked. Discoverability is a problem sometimes on crates.io. It looks a bit fuller-featured than my lib.

Just shows the power of generics & traits, that a library like this can get any value implementing FromStr.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.