rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
97.52k stars 12.61k forks source link

unexplained Rust gotcha in Guessing Game example #56681

Closed narration-sd closed 5 years ago

narration-sd commented 5 years ago

This is due to a facet you are promoting of Rust language, but causes consternation in what is supposed to be an easy-entry example.

The problem occurs when you are invited to 'just' add a loop to your code-so-far, to allow multiple guesses. What you are likely to get if you follow other language practices is a nasty error:

thread 'main' panicked at 'Por favor, escriba un número!: ParseIntError { kind: InvalidDigit }', libcore\result.rs:1009:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: process didn't exit successfully: `target\debug\guessing_game.exe` (exit code: 101)

This occurs if you don't include the line let mut guess = String::new(); within the loop, which is likely if you have a lot of experience, and are in a hurry, especially, though no doubt newer persons could also do it. If you go on to include the better error handling later in the lesson, you'll get an equally obscure error: invalid digit found in string.

In the morning, it seems this occurs because of the type-shadowing which has made the var guess no longer able to take a String. I would also say that the error messages, either of them, are very uninformative about the real problem.

What can you do?

I was really surprised to see such a highly touted newer language being so raw. It makes me a bit wary about the intended reason to use it, Web Assembly. I hope you'll improve, thanks.

steveklabnik commented 5 years ago

Can you please re-file this against https://github.com/rust-lang/book? Thank you!

steveklabnik commented 5 years ago

Actually, I guess this really isn't about the contents of the book, it's more about the diagnostics. So let's leave it here. Sorry for the noise.

narration-sd commented 5 years ago

good man. And I edited a little to pick up own mis-types...

estebank commented 5 years ago

@steveklabnik I don't see how this is a diagnostics issue as it happens on runtime: someone writing this code from scratch would either have to deal with parse returning a Result::Err or know enough about what expect/unwrap is doing.


@narration-sd the book is using a blunt tool for error handling in order to keep the code simple. It is a very reasonable tradeoff. In real code you would deal with the result of

    let guess: Result<u32, ParseIntError> = guess.trim().parse();
    let guess: u32 = match guess {
        Ok(val) => val,
        Err(ParseIntError { kind: InvalidDigit}) => {
            println!("Por favor, escriba un número!");
            ::std::process::exit(1);
        }
        Err(err) => {
            println!("Se ha encontrado un error: {:?}", err);
            ::std::process::exit(1);
        }
    };

instead of failing outright when that parse has failed, as in the book:

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

There's no line number for the code error -- this is basic, right?

This is because it isn't a compile time error, and there are reasons for panics to not carry more information than they already do.

'panicked' ?? come on...

It is the name of the unwinding mechanism in rust, the result of calling panic!() or expect on a Result.

it's not an invalid digit, it's an invalid type String trying to be digits....you should say so

The error is pointing that the valid String has invalid digits when trying to parse it as an u32.

the RUST_BACKTRACE is actually an env var, thus unclear

Perfectly fair.

the 'better' error message is just wrong. The input isn't bad because it's something wrong in the String; it's bad because the shadowed var can't take a String at all...

I believe you might be indeed misunderstanding where the problem is. It is not a type level problem, the string is still a valid string, the error is being caused when trying to call parse on a String that holds something that cannot be converted to a u32. If you remove the expect() call you will see that the result of parse can either be Ok(u32_val) or Err(IntParseErr). The later is the one being represented as the string invalid digit found in string.


I believe there are two actionable points are:

narration-sd commented 5 years ago

hmm hmm. Nice analysis, @estebank; however, it doesn't seem in important regards to quite fit.

Here are a few things I thought of after the original issue:

Thanks, and Rust is interesting... Clive p.s. read_line() -- déjà vu, feel like I'm back in Pascal for a moment ;)

estebank commented 5 years ago

It would be useful to have a copy of the code that was causing you trouble, that way we could devise further improvements to diagnostics that could help other newcomers.

narration-sd commented 5 years ago

Sure -- as follows.

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Adivina el número!");

    let secret_number = rand::thread_rng().gen_range(1, 101);
    println!("El número secreto es: {}", secret_number);

    // here's the problem start -- initialization thus first type set outside of the loop
    let mut guess = String::new();

    loop {

        println!("Por favor ingrese su conjetura.");

        io::stdin().read_line(&mut guess)
            .expect("Error al leer la línea...");

        println!("Usted lo adivinó: {}", guess);

//        let guess: u32 = guess.trim().parse()
//            .expect("Por favor, escriba un número!");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(err) => {
                println!("Número malo! {:?} : Inténtalo de nuevo...", err);
                continue
            },
        };

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("¡Demasiado pequeña!"),
            Ordering::Greater => println!("¡Demasiado grande!"),
            Ordering::Equal => {
                println!("¡Tú ganas!");
                break;
            },
        }
    }
}
narration-sd commented 5 years ago

n.b. I've possibly made less clear by explaining too much, but the basis problem appears to be that once guess has been shadowed, it can no longer accept a string.

Then the parse fails, not because of an actual improper number, but because guess now is flagged, or contains something parse can't handle.

I did pick up your {:?} for better general error information...thanks.

estebank commented 5 years ago

The problem you have here is not one of shadowing, but it is indeed a wart of the api:

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Adivina el número!");

    let secret_number = rand::thread_rng().gen_range(1, 101);
    println!("El número secreto es: {}", secret_number);

    // here's the problem start -- initialization thus first type set outside of the loop
    let mut guess = String::new();

    loop {
        guess.clear()  // <- NOTE THIS: you have to clear the `guess` buffer after every use, otherwise `read_line` will *append* to it
        println!("Por favor ingrese su conjetura.");

        io::stdin().read_line(&mut guess)
            .expect("Error al leer la línea...");

        println!("Usted lo adivinó: {}", guess);

//        let guess: u32 = guess.trim().parse()
//            .expect("Por favor, escriba un número!");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(err) => {
                println!("Número malo! {:?} : Inténtalo de nuevo...", err);
                continue
            },
        };

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("¡Demasiado pequeña!"),
            Ordering::Greater => println!("¡Demasiado grande!"),
            Ordering::Equal => {
                println!("¡Tú ganas!");
                break;
            },
        }
    }
}

You're appending to the buffer guess, you need to clear it. If you look at the output, if you try 10\n and then 15\n, the output will be Usted lo adivinó: 10\n15\n.


Filed https://github.com/rust-lang/rust/issues/56734 to deal with the env var feedback.

narration-sd commented 5 years ago

Fair enough -- though as you appear to recognize, the last things I'd expect from semantics of an api function named read_line are:

Then, github just updated on-the-page, so see you've picked up on first point, though 'identifying' seems it's short of the mark as far as how this should act, unless as ever I am missing something.

Thanks, Esteban, and hope I'm contributing to development of clarity of Rust here. C.

narration-sd commented 5 years ago

added to, if you are reading only email...

narration-sd commented 5 years ago

ok, last on this I think. I had a look through old postings on Rust w/r/t read_line's behavior and related matters, not to say the doc.

I guess all this I ran into is intentional, in stdio. And also non-intuitive to many of experience.

Ok, got that off my chest, and maybe I have to accept Rust as a specific-corners sort of language, useful for its uses however.

Thanks for your patience to see this through, @estebank , and indeed shadowing itself seems to work as I'd thought it should in this case -- will have to read more about that in doc.

Regards, Clive

narration-sd commented 5 years ago

p.s. for other unenlightened pre-rusters, this would have been helpful to go through.

It does rather illustrate that I wasn't out on a limb to find things to be unusual...but also will help to get 'there'...

http://dtrace.org/blogs/ahl/2015/06/22/first-rust-program-pain/

The various issues with read_line() are right up front, and he substitutes around them. As I don't think they're very comfortable to resolve.

Mohammad-Idrees commented 2 months ago

The problem you have here is not one of shadowing, but it is indeed a wart of the api:

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Adivina el número!");

    let secret_number = rand::thread_rng().gen_range(1, 101);
    println!("El número secreto es: {}", secret_number);

    // here's the problem start -- initialization thus first type set outside of the loop
    let mut guess = String::new();

    loop {
        guess.clear()  // <- NOTE THIS: you have to clear the `guess` buffer after every use, otherwise `read_line` will *append* to it
        println!("Por favor ingrese su conjetura.");

        io::stdin().read_line(&mut guess)
            .expect("Error al leer la línea...");

        println!("Usted lo adivinó: {}", guess);

//        let guess: u32 = guess.trim().parse()
//            .expect("Por favor, escriba un número!");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(err) => {
                println!("Número malo! {:?} : Inténtalo de nuevo...", err);
                continue
            },
        };

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("¡Demasiado pequeña!"),
            Ordering::Greater => println!("¡Demasiado grande!"),
            Ordering::Equal => {
                println!("¡Tú ganas!");
                break;
            },
        }
    }
}

You're appending to the buffer guess, you need to clear it. If you look at the output, if you try 10\n and then 15\n, the output will be Usted lo adivinó: 10\n15\n.

@estebank How is this code working after first iteration if we are shadowing the guess to u32?