Skip to content

[Draft] RFC: Console Input Simplified #3183

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from

Conversation

undersquire
Copy link

@undersquire undersquire commented Oct 17, 2021

Implement simple macros to make console input simple, just like how console output via println is very simple and convenient.

Rendered

EDIT: This RFC is not complete and the concept for the API has changed since the one in the rendered document. I will update the RFC document once an API is agreed upon and a basic implementation is written for it.

For those curious of the current API, check my repo for the most current API and the WIP implementation.

@m1ten
Copy link

m1ten commented Oct 17, 2021

I like the idea. Input and output would be more consistent if input! gets implemented.

@kennytm
Copy link
Member

kennytm commented Oct 18, 2021

see also:

@undersquire
Copy link
Author

undersquire commented Oct 19, 2021

see also:

In the second link, they closed the issue with the message: This was discussed in today's meeting and it was decided that a feature such as this should bake in a library before being accepted into the main repo, so I'm going to close this for now.

This feature has been baking in several libraries (such as text_io) for several years, so maybe I could suggest the version from one of these crates?

@dlight
Copy link

dlight commented Oct 21, 2021

I think inputln!() doesn't need to be a macro, since it would work just as well if it were a function (maybe with #[inline]). Nonetheless, I think it's really important to have something like this in the stdlib for writing short snippets of code. I mean, here's the most compact code I can think of that does the same thing (well, sans error handling) without needing multiple statements:

use std::io::BufRead;

std::io::stdin().lock().lines().next().unwrap().unwrap()

Also, being able to write this kind of thing without having to call Stdin::read_line() is also important for teaching, because it lets us read lines from stdin working purely with value semantics, before we learned even what a &mut is. Indeed after teaching inputln!(), the teacher could then show how the pros do it, and explain that while Stdin::read_line() is a more complicated API, there's a benefit if we want to reuse the String buffer for reading multiple lines.

@dlight
Copy link

dlight commented Oct 21, 2021

Something that is not clear in the guide-level explanation of input!() is that it reads the whole line. On a first impression, I would expect it to read up to the next whitespace, at least for numbers. that way, one could call input!(i32) multiple times to read numbers in the same line. (this was my expectation at first because that's what C's scanf("%d") does).

That's specially confusing because input!() and inputln!() seem to be analogous to print!() and println!(), but print!() is just a println!() that prints without a newline. So it's logical to expect that input!() reads without a newline, too. Indeed, from the naming alone, what I expected is that inputln!() also received a type parameter, so that inputln!(i32) reads a line and parses it as i32, and input!(i32) reads until the first whitespace. inputln!() without a parameter could still work though, and be equivalent to inputln!(String).

But I think this just means that input!() and inputln!() are bad names, or, at least, the names don't mirror print!() and println!() as much as one would like.

And well, scanf is arguably a bad API, and it does the wrong thing when running scanf("%s") - it reads until the first whitespace, which is a common a newbie trap, and most programs that read strings from stdin want to read the whole line (and then maybe split it afterwards and treat each piece separately), because a tty is line-buffered. So input!(String) reading only up to the first whitespace character is the wrong thing to do.

@chrysn
Copy link

chrysn commented Oct 21, 2021

A reason for Python's prompt line is that the input needs to be flushed. Just going for

    print!("Name? ");
    let name = input!(String);

doesn't show the prompt until after the user has pressed Return. A println! works, but given that input-in-the-same-line is a style that's frequent in interactive CLI tools, it doesn't quite cut it.

@chrysn
Copy link

chrysn commented Oct 21, 2021

On the "drawback" side, I'd add that it is putting a possibly sub-optimal solution into the standard library.

Printing to stdout is straightforward, and print/println can cover the common operations with good quality, possibly even with color, up to the point where there is binary data or you want fancy line rewriting.

Reading from stdin does not have an analogon for "simple and does almost everything you want": The terminal emulator usually gives some line editing capability (like erasing the last character), but even moving back with the cursor to change anything but the last letter is not provided. The Python version you cite for comparison does not do that either, but as soon as the Python readline module is loaded (which is the case when the interactive interpreter is used), readline's input takes over Python's input function and provides not only cursor movement but also history.

Other than first-week introduction-to-programming examples, I think that most developers will have unhappy users with a bare terminal input, and would be better off using rustyline or something like that right away.

@undersquire
Copy link
Author

Something that is not clear in the guide-level explanation of input!() is that it reads the whole line. On a first impression, I would expect it to read up to the next whitespace, at least for numbers. that way, one could call input!(i32) multiple times to read numbers in the same line. (this was my expectation at first because that's what C's scanf("%d") does).

That's specially confusing because input!() and inputln!() seem to be analogous to print!() and println!(), but print!() is just a println!() that prints without a newline. So it's logical to expect that input!() reads without a newline, too. Indeed, from the naming alone, what I expected is that inputln!() also received a type parameter, so that inputln!(i32) reads a line and parses it as i32, and input!(i32) reads until the first whitespace. inputln!() without a parameter could still work though, and be equivalent to inputln!(String).

But I think this just means that input!() and inputln!() are bad names, or, at least, the names don't mirror print!() and println!() as much as one would like.

And well, scanf is arguably a bad API, and it does the wrong thing when running scanf("%s") - it reads until the first whitespace, which is a common a newbie trap, and most programs that read strings from stdin want to read the whole line (and then maybe split it afterwards and treat each piece separately), because a tty is line-buffered. So input!(String) reading only up to the first whitespace character is the wrong thing to do.

Yes that is why in the RFC I state that it would be better for input! to use more of a C++ std::cin approach, where it reads to the next space rather than the whole line: One thing I would probably do differently in this implementation is, for the input!(TYPE); macro, make it so it only reads from stdin until it hits a (space), similar to how C++'s std::cin works.

However, I have been revising my RFC and trying to come up with a better API, and found the one found in the text_io crate to be quite useful and powerful. Maybe we can go with it's approach?

@undersquire
Copy link
Author

On the "drawback" side, I'd add that it is putting a possibly sub-optimal solution into the standard library.

Printing to stdout is straightforward, and print/println can cover the common operations with good quality, possibly even with color, up to the point where there is binary data or you want fancy line rewriting.

Reading from stdin does not have an analogon for "simple and does almost everything you want": The terminal emulator usually gives some line editing capability (like erasing the last character), but even moving back with the cursor to change anything but the last letter is not provided. The Python version you cite for comparison does not do that either, but as soon as the Python readline module is loaded (which is the case when the interactive interpreter is used), readline's input takes over Python's input function and provides not only cursor movement but also history.

Other than first-week introduction-to-programming examples, I think that most developers will have unhappy users with a bare terminal input, and would be better off using rustyline or something like that right away.

I don't think they would be unhappy, I think they would simply just install a more complex crate if they need more features. Having a simplified input system would simply make input for smaller programs, teaching, etc easier. I understand what you mean however.

@chrysn
Copy link

chrysn commented Oct 22, 2021

The users are in no position to updated that -- unlike Python where you can just import readline around the program, when a developer uses the std input the users are stuck with that. And these users would be stuck in a three-party triangle of

  • users who are told that this is good enough but see helllo^[[world on screen and yet different program behavior,
  • developers who don't want to use an external crate when the standard library already does it, and
  • the standard library team that is asked to "just make it bettter" (which is not trivial portably, which is exactly why things like readline exist).

I wouldn't want to be in either of these three situations.

If we go ahead with this, I think that the input functions should described as "for experimentation and educational purposes", and their documentation should recommend using platform specific method or third party abstractions when not falling into these categories (possibly with concrete suggestions). Then the above deadlock has a clear way out.

@undersquire
Copy link
Author

The users are in no position to updated that -- unlike Python where you can just import readline around the program, when a developer uses the std input the users are stuck with that. And these users would be stuck in a three-party triangle of

  • users who are told that this is good enough but see helllo^[[world on screen and yet different program behavior,
  • developers who don't want to use an external crate when the standard library already does it, and
  • the standard library team that is asked to "just make it bettter" (which is not trivial portably, which is exactly why things like readline exist).

I wouldn't want to be in either of these three situations.

If we go ahead with this, I think that the input functions should described as "for experimentation and educational purposes", and their documentation should recommend using platform specific method or third party abstractions when not falling into these categories (possibly with concrete suggestions). Then the above deadlock has a clear way out.

Responding to the three-party triangle:

  1. The input! macro simply reads in a value from the console, and this unexpected behavior you gave as an example exists in every language's implementation of basic console input (arrow keys writing ^[[), so I am not sure this is at all a big deal.

  2. This is not really the case. In fact, I see this as potentially a good thing. Like imagine needing to add an entire crate, increasing compile time just for something as small as println!. It is inconvenient, especially when the standard library could probably easily implement such a feature. On the other hand, people use crates over things from the standard library all the time, a big one would be asynchronous programming. Not many people write async/concurrent code using just the standard library (even though it's 100% possible and not really that difficult), you are almost guaranteed to add tokio to your dependencies or another crate. Another example is argument parsing. You can do this just fine with the standard library, but there are crates like clap for more features when needed.

  3. I am not really sure people are going to complain about a console input feature; I don't see people complaining often about println or anything like that. Either way, if we provide a good API (not saying mine is the solution, in fact I am revising my concept for it right now to change and improve it), people will probably be happy enough with it.

This is just like the println macro, it can easily be replaced by a logging crate and often is in many projects. Same with how this input! macro would be able to be replaced in many projects with crates like rustyline. The point of this macro is that it makes it more immediate to start handling input, whether used in a learned/teaching area or maybe professionally in an actual project, it still makes sense to me to offer it regardless.

@undersquire
Copy link
Author

undersquire commented Oct 23, 2021

I have just pushed a commit to this RFC improving the example implementation. It now only reads up to a space for input!(TYPE) and works quite well. For instance, if I input 10 10, and the program calls input!(i32); twice, it will obtain both integers. If I wrote 10\n10 (\n being me pressing enter), and call input!(i32); twice, I will still get both integers.

Please let me know what you think of these changes.

@voidc
Copy link

voidc commented Oct 23, 2021

See also rust-lang/rust#75435 and this draft RFC.

@m1ten
Copy link

m1ten commented Oct 23, 2021

See also rust-lang/rust#75435 and this draft RFC.

While there are other RFCs related to #3183, none of them solved the problem.

@HKalbasi
Copy link
Member

This RFC is great for using rust in competitive programming. People still widely use C/C++ in that field, because languages such as python or java will exceed time/memory limits in hard problems. So Rust is a great fit for this field, but lack of a good input system blocks the usage of Rust. And this RFC is the only possible kind of solution for it because:

  • Even something like inputln!() is suboptimal and not enough, because these problems are designed with scanf and cin in mind, and you don't want to spend your time writing code that parses spaces.
  • Third party crates are not option, because your code will be compiled in the judge servers, so you need to convince the platform owners to include this third party crate with every code. It is not impossible, but very hard. There are too many platforms and competing crates for some work (and you should not include too many crates, for fair competition with other languages), so it is not practical. With this being included in the std, the problem is solved.

Other than first-week introduction-to-programming examples, I think that most developers will have unhappy users with a bare terminal input, and would be better off using rustyline or something like that right away.

If scanf or cin or input works for people in other languages, this would work for people in Rust similarly. I doubt if only first week programmers are happy with scanf and cin in other languages.

@undersquire
Copy link
Author

This RFC is great for using rust in competitive programming. People still widely use C/C++ in that field, because languages such as python or java will exceed time/memory limits in hard problems. So Rust is a great fit for this field, but lack of a good input system blocks the usage of Rust. And this RFC is the only possible kind of solution for it because:

  • Even something like inputln!() is suboptimal and not enough, because these problems are designed with scanf and cin in mind, and you don't want to spend your time writing code that parses spaces.
  • Third party crates are not option, because your code will be compiled in the judge servers, so you need to convince the platform owners to include this third party crate with every code. It is not impossible, but very hard. There are too many platforms and competing crates for some work (and you should not include too many crates, for fair competition with other languages), so it is not practical. With this being included in the std, the problem is solved.

Other than first-week introduction-to-programming examples, I think that most developers will have unhappy users with a bare terminal input, and would be better off using rustyline or something like that right away.

If scanf or cin or input works for people in other languages, this would work for people in Rust similarly. I doubt if only first week programmers are happy with scanf and cin in other languages.

Yeah this is another good example of where having a simplified input system would be useful. My concept for the implementation is also just a mere example, I was thinking that it might be better to implement the input functions/macros that an existing crate has like text_io, as it supports more advanced input formatting. What do you think of that?

@undersquire undersquire marked this pull request as draft October 27, 2021 00:28
@undersquire
Copy link
Author

I decided to mark this as a draft since we are still deciding on an implementation. Please suggest any ideas if you have any.

@undersquire undersquire changed the title RFC: Console Input Simplified [Draft] RFC: Console Input Simplified Oct 27, 2021
@undersquire undersquire marked this pull request as ready for review November 13, 2021 19:06
@undersquire undersquire changed the title [Draft] RFC: Console Input Simplified RFC: Console Input Simplified Nov 13, 2021
@remi-dupre
Copy link

remi-dupre commented Nov 13, 2021

I've been using Rust for competitive programming for a while (while not being competitive myself).

What is usually done for common tools is to have a template that you would use to solve each problem, with Rust I have indeed had to think about the parsing issue. My approach has been to rely on generics and output structure instead of a macro: templates/rust.rs.

I think we could make this RFC more ambitious:

  • should be able to perform on any reader, not just stdin
  • it would be great to be able to output to more complex types, not just Vec<T>, do we need a macro for this?
  • would we want an even higher level tool, like C's scanf? I would imagine something like that:
let name: String;
let weight: u64;
let child_id: usize;
scanln!("name: {}, weight: {}, child_id: {}", &mut name, &mut weight, &mut child_id);

I still probably miss a lot of things.
Thanks for pushing this great idea 😄

@undersquire
Copy link
Author

undersquire commented Nov 13, 2021

I've been using Rust for competitive programming for a while (while not being competitive myself).

What is usually done for common tools is to have a template that you would use to solve each problem, with Rust I have indeed had to think about the parsing issue. My approach has been to rely on generics and output structure instead of a macro: templates/rust.rs.

I think we could make this RFC more ambitious:

  • should be able to perform on any reader, not just stdin
  • it would be great to be able to output to more complex types, not just Vec<T>, do we need a macro for this?
  • would we want an even higher level tool, like C's scanf? I would imagine something like that:
let name: String;
let weight: u64;
let child_id: usize;
scanln!("name: {}, weight: {}, child_id: {}", &mut name, &mut weight, &mut child_id);

I still probably miss a lot of things. Thanks for pushing this great idea 😄

I like your idea, it makes it more powerful. However, how would the string work here? When you call scanln!("name: {}", &mut name); for example, does it print out name: and then reads input? Or is the user expected to write name: INPUT?

I will attempt to write an implementation for this too and once I get it working I will post it here.

With regards to your first idea, I think we should also propose a read and readln macro (like I mentioned in the rendered RFC). These would take in a stream to read from, so you could do something like readln!(file, "name: {}", &mut name);.

@OvermindDL1
Copy link

OvermindDL1 commented Nov 13, 2021

I like scan! and scanln! as well. As for the example above I'd expect it to work like how regex does (honestly regex support would be amazing but obviously not in the std, but some trait for the string part that the regex library could implement would be amazing) where the string tried to greedily consume the whole line (since scanln gets a whole line) but the rest failed so it backtracked until it all works.

I'm not a fan of input and so forth as described above for a variety of reasons, including it just looks weird (should be a template into something, like a normal function, not a macro, or why can't it know the type to parse into based on what it's being saved into).

Still seems like this should be a create to start with though until it's well tested rather than an RFC?

@undersquire
Copy link
Author

Yeah I'm thinking of just turning this into a crate for a bit while I try to find the best API for this. At the moment, I am experimenting with something that looks like this:

fn main() {
    let age: u8 = scan!().unwrap();

    // we can read with a pattern aswell
    let name: String = scan!("name: {}").unwrap();

    // im hoping to get something like this working
    let person: (String, u8) = scan!("{}, {}").unwrap();
}

What do you think?

@mikeleany
Copy link

@undersquire said

I know that not everyone here agrees with the type being specified as an argument in the macro, however this was the only way to achieve reading multiple values at once that I could figure out. It isn't (imo) bad however, it looks consistent with the println macro.

println!("FORMAT HERE", var1, var2, ...);
scan!("FORMAT HERE", type1, type2, ...);

Why not put the variables as arguments to the scan! macro (as in @remi-dupre's example) instead of the types? This looks better to me and is more consistent with print! and with C's scanf and friends. There would, of course, be the downside that they would need to be declared before scan! was called, but I think the improved readability makes up for that.

@undersquire
Copy link
Author

@undersquire said

I know that not everyone here agrees with the type being specified as an argument in the macro, however this was the only way to achieve reading multiple values at once that I could figure out. It isn't (imo) bad however, it looks consistent with the println macro.
println!("FORMAT HERE", var1, var2, ...);
scan!("FORMAT HERE", type1, type2, ...);

Why not put the variables as arguments to the scan! macro (as in @remi-dupre's example) instead of the types? This looks better to me and is more consistent with print! and with C's scanf and friends. There would, of course, be the downside that they would need to be declared before scan! was called, but I think the improved readability makes up for that.

Maybe I could support both, because to me it seems easier to just write the types and get back a tuple than having to declare each individual value first. I will add the implementation for both into my crate for testing so we can experiment with it and see which one (or maybe both versions) is the best.

@mikeleany
Copy link

After thinking about it a minute more, I think I would actually love to see something like this, with the type specified in the braces:

let (name, age) = scan!("{String}, {u8}"); // returns `Result<(String, u8), Error>`

And maybe allowing something like this:

let (name, age);
scan!("{name}, {age}") // returns `Result<(), Error>`

@undersquire
Copy link
Author

After thinking about it a minute more, I think I would actually love to see something like this, with the type specified in the braces:

let (name, age) = scan!("{String}, {u8}"); // returns `Result<(String, u8), Error>`

And maybe allowing something like this:

let (name, age);
scan!("{name}, {age}") // returns `Result<(), Error>`

This would be nice, but unfortunately not likely possible. I have been experimenting with macros and I tried to create something like this but I was unable to. Maybe someone else here more experienced knows how to implement this.

@mikeleany
Copy link

After thinking about it a minute more, I think I would actually love to see something like this, with the type specified in the braces:

let (name, age) = scan!("{String}, {u8}"); // returns `Result<(String, u8), Error>`

And maybe allowing something like this:

let (name, age);
scan!("{name}, {age}") // returns `Result<(), Error>`

This would be nice, but unfortunately not likely possible. I have been experimenting with macros and I tried to create something like this but I was unable to. Maybe someone else here more experienced knows how to implement this.

RFC 2795 does something similar. Have you looked at how that's implemented?

@undersquire
Copy link
Author

After thinking about it a minute more, I think I would actually love to see something like this, with the type specified in the braces:

let (name, age) = scan!("{String}, {u8}"); // returns `Result<(String, u8), Error>`

And maybe allowing something like this:

let (name, age);
scan!("{name}, {age}") // returns `Result<(), Error>`

This would be nice, but unfortunately not likely possible. I have been experimenting with macros and I tried to create something like this but I was unable to. Maybe someone else here more experienced knows how to implement this.

RFC 2795 does something similar. Have you looked at how that's implemented?

This seemed to be implemented at the compiler level, as the compiler could read in the format string at compile time and pick out the variable names. So I guess if the compiler developers were willing to implement this at that level then we can have this API.

@undersquire
Copy link
Author

undersquire commented Nov 15, 2021

It isn't complete, but here is a link to the repository of the crate I am writing for testing implementations for this RFC.

(NOTE: This isn't functional at the moment, should be by the end of tomorrow).
https://github.com/undersquire/scanio

This is mainly for anyone who wants to contribute to the implementation or look over how I am implementing it.

@jhpratt
Copy link
Member

jhpratt commented Nov 15, 2021

Why does the outcome of #67 not apply here? I've no problem with there being discussion over design, but it shouldn't be taking place in this repository until there's something that could be adopted nearly verbatim.

@m1ten
Copy link

m1ten commented Nov 15, 2021

Why does the outcome of #67 not apply here?

Outcome of #67 does apply here, but now there is an implementation being worked on unlike before. The previous RFC was closed due it not being a library and couldn't be tested. Several libraries have been created since then and I would assume things to change in 7 years.

Edit: Also because the problem still exists. Nothing has been done to provide simple input like there is in pretty much every other language.

@jhpratt
Copy link
Member

jhpratt commented Nov 15, 2021

If there's already a library doing this, why is there so much uncertainty in this thread? New ideas are being thrown out such as a scan! macro. That doesn't indicate this is anywhere near ready for std.

@m1ten
Copy link

m1ten commented Nov 15, 2021

If there's already a library doing this, why is there so much uncertainty in this thread? New ideas are being thrown out such as a scan! macro. That doesn't indicate this is anywhere near ready for std.

That's just opinion based. Some people like scan! macro, others don't. I mean we can't stop people from sharing their ideas.

@undersquire
Copy link
Author

undersquire commented Nov 15, 2021

If there's already a library doing this, why is there so much uncertainty in this thread? New ideas are being thrown out such as a scan! macro. That doesn't indicate this is anywhere near ready for std.

I understand what you mean, however the main reason I am keeping this RFC open even though the implementation isn't fully agreed upon is because otherwise this will just die out like the original RFC #67 you mentioned. That RFC was opened 7 years ago and yet we still have nothing. If by keeping this RFC open it keeps people active on the issue then there is a good chance it will be solved.

EDIT: I am going to draft this RFC for now since there isn't a decision on the implementation yet.

@undersquire undersquire marked this pull request as draft November 15, 2021 04:42
@undersquire undersquire changed the title RFC: Console Input Simplified [Draft] RFC: Console Input Simplified Nov 15, 2021
@not-my-profile
Copy link

not-my-profile commented Nov 16, 2021

I agree with @dlight's comment:

Also, being able to write this kind of thing without having to call Stdin::read_line() is also important for teaching, because it lets us read lines from stdin working purely with value semantics, before we learned even what a &mut is.

Building an interactive console game is a great way to familiarize yourself with a new language and std::io::Stdin::read_line can be intimidating to programmers used to the simplicity of Python's input().

So I think the suggested inputln!() macro would be a fine addition to the standard library (except that it should probably be a function instead and definitely should return a std::io::Error error type instead of ()).

I however do not see the appeal of the suggested input!() macro.

let age = input!(i32).expect("Invalid age!");

That in my humble opinion is strictly less readable/desirable than:

let age: i32 = inputln!()?.parse().expect("Invalid age!");

I don't consider a helper to parse multiple values from a string to be something that needs to be in the standard library. I think such parsing utilities should be best left to crates.

tl;dr: I think the best argument for this RFC (making Rust more accessible to beginners) would be better addressed by an inputln() -> std::io::Result<String> function. And I would support an RFC proposing it.

@undersquire
Copy link
Author

I agree with @dlight's comment:

Also, being able to write this kind of thing without having to call Stdin::read_line() is also important for teaching, because it lets us read lines from stdin working purely with value semantics, before we learned even what a &mut is.

Building an interactive console game is a great way to familiarize yourself with a new language and std::io::Stdin::read_line can be intimidating to programmers used to the simplicity of Python's input().

So I think the suggested inputln!() macro would be a fine addition to the standard library (except that it should return a std::io::Error error type instead of ()).

I however do not see the appeal of the suggested input!() macro.

let age = input!(i32).expect("Invalid age!");

That in my humble opinion is strictly less readable/desirable than:

let age: i32 = inputln!()?.parse().expect("Invalid age!");

I don't consider a helper to parse multiple values from a string to be something that needs to be in the standard library. I think such parsing utilities should be best left to crates.

tl;dr: I think the best argument for this RFC (making Rust more accessible to beginners) would be better addressed by an inputln!() -> std::io::Result<String> macro. And I would support an RFC proposing it.

I think that a simpler function like std::inputln() -> std::io::Result<String> would be nice to have and much easier to implement, but based on other responses it seems people favor a more advanced approach like scan! etc.

Should I create a poll to decide whether we should do a simpler or more advanced approach and just let it to a vote?

@H2CO3
Copy link

H2CO3 commented Nov 16, 2021

@HKalbasi:

If scanf or cin or input works for people in other languages, this would work for people in Rust similarly. I doubt if only first week programmers are happy with scanf and cin in other languages.

It doesn't work though. Go on any C or C++ forum or Stack Overflow. The questions will be full of confused beginners abusing scanf() and wondering why cin >> string stops at the first whitespace. Parsing is non-trivial, and trying to hide it behind "easy" APIs is no better than OO languages trying to hide shared mutability behind "easy-to-use objects". It has absolutely no place in the standard library of a correctness-oriented language.

@clarfonthey
Copy link

I've been neglecting commenting on this thread because I've wanted to read it all before I say things, but at this point I don't have the time so I'll just list my thoughts here, whether they're repeats or not:

Basically, I've felt for a while that FromStr really sucks, especially with TryFrom existing. With TryFrom, because things are passed by-value, you can specify lifetimes to allow borrowing parts of the string in both the value and the error; FromStr explicitly does not allow this, since the return type must be the same for all possible lifetimes of &str.

That said, there is clearly a need for some form of FromStr. There are libraries which implement both TryFrom<&str> and FromStr because the semantics are different -- TryFrom is a conversion, and FromStr is a parse. For example, TryFrom<&str> for a JSON type would create a JSON string with the value of the string; FromStr for a JSON string would parse the string as JSON.

Another thing that TryFrom also has is the ability to parse non-UTF-8 output, which would be needed for parsing I/O input. You can easily do TryFrom<&[u8]>, TryFrom<&[u16]>, and even TryFrom<&OsStr> and TryFrom<&Path> today; this is not possible with FromStr.

I think that FromStr should be deprecated and replaced by a version which solves these problems, or at least allows explicitly borrowing the string input as part of the returned Result, either in the value or the error. The error part is particularly important, since it lets you reference the failed parts of the input without having to allocate a new String. I don't know what this should look like, but since this API explicitly relies on FromStr, it's an instant no from me for that reason.

I personally think that the order of steps should be to replace FromStr first with a better equivalent, then figure out if there's a way to make reading from stdio easier.

@BartMassey
Copy link

@undersquire Thanks for getting this going.

It seems to me that the idea as it currently exists in your repo is heading in a different direction than what the RFC started with. I see a couple of distinct needs here:

  1. scanf-like functionality for Rust
  2. User-friendly line input for Rust

I don't have strong opinions on (1): I think it's hard to get this interface right and it deserves its own RFC and discussion. I would really like to separate it from (2). For me the motivating example is Guessing Game from the Rust book, which avoids trying to do anything clean code-wise or usage-wise with getting a guess because zomg Rust line input.

I have previously suggested standardizing on the functionality of my prompted crate as a solution for (2). I think it covers a lot of that space. tl;dr of prompted:

let line1 = input!();
let line2 = input!("{}: ", 2);

The input!() macro currently returns a String. If people feel like it's really important for the input!() macro to be named inputln!() that's fine. If people feel like it's important that input!() return a Result<String, Error> or Result<T: FromStr, Error> that's fine too — I'm not sure whether the ergonomics of that are better or worse than separating out the .parse() and panicking on failure. I do feel strongly that providing a Python-like easy-mode way to provide a prompt for input and have it properly displayed is pretty necessary: the current Rust solution is just hard and error-prone.

If people like this idea we can try to get together an RFC for it. I would then suggest a separate discussion of (1). I think having a solid scanf-like crate is a prerequisite to any potential std functionality: there are many crates that might serve as a good starting point.

@not-my-profile
Copy link

I agree that user-friendly line input is a separate issue from parsing/scanning and should therefore be discussed separately. Which is also the reason I opened my RFC #3196 for which I explicitly declared parsing/scanning as out of scope.

You're of course welcome to comment on my RFC :) Please however mind that my RFC proposes a function as opposed to a macro and that the function vs macro matter has already been discussed extensively.

@undersquire
Copy link
Author

undersquire commented Nov 25, 2021

I actually made a poll a little while ago to see what the community prefers, and it seems that they prefer that the standard library offered both simple and advanced input APIs.

So I think that the best course of action is to get #3196 implemented/accepted. From there we can look into a more advanced API for the cases where you need more advanced input. I am uncertain what it would look like but I think a dedicated RFC for it will spark a discussion.

@lebensterben
Copy link

Who participated in the poll?
It might be just a skewed sample at best, or a selected sample at worst.

@undersquire
Copy link
Author

Who participated in the poll?

I posted it to the official Rust discord and another unofficial one, and r/rust

@iago-lito
Copy link

Well why not here? I would have been glad to participate, had I been aware :(

@undersquire
Copy link
Author

Well why not here? I would have been glad to participate, had I been aware :(

I thought about posting it here but didn't think many people would have voted anyway. Posting it on the active discords and subreddits guaranteed a larger sample size. If you want, you can still vote :P

@rdev32
Copy link

rdev32 commented Feb 19, 2022

// current w/ verbose
std::io::stdin().read_line(&mut input).expect("Error");
// quick fix (?)
--> std::io::read(&mut input).expect("Error");
--> std::io::readln(&mut input).expect("Error");
// suggestion
--> readln!(mut& input);
// since println!() already exists

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-libs-api Relevant to the library API team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.