Simple console input macros proposal

I made an draft RFC about a month ago proposing some useful input macros to accompany println! family of macros, and over time people suggested new and better API's and ideas which helped evolve the proposal API quite a bit. I wrote and released a crate today implementing the most current API that we've came up with in the responses to my RFC, and I wanted to hear your opinions on it.

The repository for my crate scanio that implements the current API is located here: GitHub - undersquire/scanio: Simple console input macros with the goal of being implemented in the standard library.

The RFC is located here if you want more information: [Draft] RFC: Console Input Simplified by undersquire · Pull Request #3183 · rust-lang/rfcs · GitHub

The crate can be found on crates,io if you wanted to try it out in a project for yourself. Please give me your opinion on what you think about this API and the implementation I wrote for it. If enough people are happy with the API then I will update the RFC to reflect this and once ready I will mark the RFC ready to be reviewed.

I responded to your RFC on GitHub. :slight_smile:

I have three technical issues with your crate:

  • returning () as the error type is a no-go ... especially for something proposed to be in the standard library
  • I would expect the scan macro to also work with a given &mut dyn std::io::Read, instead of only supporting std::io::stdin() ... any parsing library should be independent from its input stream
  • I don't think having a panicking scan!() and a non-panicking try_scan!() is a good idea. If write!() returns an io::Result then scan!() should also do the same. Consistency is very important in API design (especially for a standard library).

But even if you address these concerns, I still don't think that such a scan macro should be added to the standard library. I believe that a std::inputln() -> std::io::Result<String> function would be a better addition to the standard library and still "make console input simple".

3 Likes

I agree that I need to add a proper error type, however your other points are already addressed. I offer scan and read! and try_read! macros, read macros being the one that can take in any &mut std::io::Read with the same API. And scan! does not panic, it simply assigns Default::default() to the provided variables if it fails. try_scan! returns a result.

Your idea for a simple std::inputln function is interesting however, as it expands on the original proposal I made in the RFC. If that's all that is needed and people are happy with a simpler implementation then I think that this is probably the best way to do this, however, based on the amount of dislikes on my original proposal it seems that people wanted a more advanced approach like the scan! one I proposed

EDIT: Do you think I should make a poll to ask if people prefer a simple approach like a std::inputln or a more advanced approach like scan!, try_scan!, etc?

1 Like

I offer scan and read! and try_read! macros

Ah my bad ... I missed those ... I only read the code blocks in your README.

Still having four macros for essentially the same thing is way too much:

  • scan!()
  • try_scan!()
  • read!()
  • try_read!()

That's too much mental overload...

And scan! does not panic, it simply assigns Default::default() to the provided variables if it fails.

Is there precedent for a Rust standard library function/macro defaulting to Default::default()?
This seems oddly unrusty to me ... a large part of the appeal of Rust for me is that error handling is explicit (as opposed to e.g. Go where everything defaults to a default type).

Your idea for a simple std::inputln function is interesting however, as it expands on the original proposal I made in the RFC.

I just realized that there currently aren't any functions in the std module ... so for the sake of consistency inputln should probably also be a macro. What was your original proposal?

Do you think I should make a poll to ask if people prefer a simple approach like a std::inputln or a more advanced approach like scan! , try_scan!, etc?

I think opening a new RFC PR for just introducing std::inputln! would be a good idea. Then people can upvote it if they agree with the idea.

I can open the RFC ... I think I can put together a more convincing Motivation section.

Yeah I can agree with that lol. I think four macros is a bit too much, however if I was able to combine it down into just one or two would it be better in that regard?

I wasn't sure what to do when implementing the macros that write to existing variables (scan!, read!), since when it fails it will essentially just not write anything to them. I think a better solution would be to have scan! and read! also return a Result, however there would be no value to return in the result so it would really just be there for explicit error checking (Result<(), ErrorType>).

A response from the lead developer in my RFC stated that making my original proposal a macro wasn't necessary and that a function would be fine. It feels inconsistent but for something like std::inputln there isn't really any justification for it to be a macro, so I think we are fine in that regards.

I can probably do that, only that it feels a little redundant since I already have another one open.

That works, it should be easy implement inputln function too:

pub fn inputln() -> std::io::Result<String> {
    let mut input = String::new();

    match std::io::stdin().read_line(&mut input) {
        Ok(_) => return Ok(input),
        Err(x) => return Err(x),
    }
}

In your RFC (if you open one) you can also suggest making it a macro for consistency however I think most people will think it is unnecessary.

EDIT: It might still be worth making a poll to capture the community's opinion at large, since this topic seems to be very controversial.

1 Like

however if I was able to combine it down into just one or two would it be better in that regard?

Yes I think that would be better suited for a (standard) library.

I wasn't sure what to do when implementing the macros that write to existing variables ( scan! , read! ),

imho a Rust library function should on failure either return a Result::Err or panic ... nothing else

EDIT: It might still be worth making a poll to capture the community's opinion at large, since this topic seems to be very controversial.

I think the :+1: and :-1: on the RFC PRs is the established polling system. Of course you can also gauge community support by asking for reactions to a comment but then those reactions won't show up when sorting RFC PRs by :+1:.

In your RFC (if you open one)

I'll try to come up with one today.

Thanks for elaborating on the macro vs function situation :slight_smile:

I created a straw poll for this in case we wanted to get poll results. If you don't think it is necessary we don't have to share it around but I think it might be beneficial.

EDIT: I shared the poll in the Rust discord so people can vote for it. I am honestly just curious to see what the community prefers when it comes to this.

Well I just opened RFC 3196 ... but it already has its first downvote, so I guess there's that :smiley:

This drove me nuts in C (scanf()) and C++ (std::istream::operator>>). I expressed my distaste with such APIs on C and C++ platforms, but here it is for URLO: There is no such thing as "formatted input".

When you are taking user input, you either treat it as a black box (so just a String), or you need to parse it. The first case is trivial, and already solved using Stdin::read_line().

In the second case, conflating input and parsing is something that has empirically caused a lot of pain, because it is hard to implement (and use) correctly, and easy to use incorrectly. Therefore, I am strongly opposed to adding any sort of parsing-as-reading functionality to std. If you need to interpret structured output, then define a format to parse, write a parser, and maybe do it all at once or line-by-line using a FromStr or Deserialize implementation.

Having the scanners panic is a complete no-go. I/O is inherently fallible, and your program must deal with it. Making I/O routines panicking is a footgun. Not everyone writes interactive CLI applications. In fact, I'd argue that anything that processes nontrivial input, other than interpreters/REPLs, should not be interactive and should instead take command line arguments and/or proper, structured input from files or a pipe.

2 Likes

The first case is trivial, and already solved using Stdin::read_line() .

In the motivation of my RFC, I argue that Stdin::read_line() is not trivial for complete Rust beginners because it requires both mutability and borrowing. And I believe that it should be, so that Rust can be more easily taught by introducing one concept after the other instead of having to say "don't worry about that for now".

Sorry, I just don't buy that. There have been numerous complaints in that spirit. "Rust is hard so we should change the language and/or the libraries such that X and Y". That is simply the wrong direction.

Borrowing is not rocket science, and it is the very cornerstone of the language. If someone finds it hard, they should try to learn the concepts rather than keep putting it off for as long as possible.

Rust doesn't have to be the first language one learns anyway. It helps a lot if you have written C or C++ and debugged memory errors, and if you have at least seen a statically-typed pure functional language (especially algebraic types and generics) before digging into Rust, because most of those concepts take time to internalize even separately.

If you want to teach Rust to complete beginners by writing interactive CLI programs, you can go ahead and write your own "easy" input() function in a couple of lines, and then your students can use it:

fn input() -> String {
    let mut s = String::new();
    std::io::stdin().read_line(&mut s).expect("input failed");
    s
}

But this is nowhere near fundamental or important enough to warrant polluting std.

1 Like

If someone finds [borrowing] hard, they should try to learn the concepts rather than keep putting it off for as long as possible.

I am not suggesting that anybody should put off learning borrowing. I'm just saying that it's better to introduce it after getting a complete Rust beginner to run their first interactive program.

Rust doesn't have to be the first language one learns anyway. It helps a lot if you have written C or C++ and debugged memory errors [...]

As Rust is getting more popular there will be an increasing amount of programmers coming from garbage-collected languages like Java and Python.

If you want to teach Rust to complete beginners by writing interactive CLI programs, you can go ahead and write your own "easy" input() function in a couple of lines, and then your students can use it.

But of course you can! But if you have ever taught a class you'll know that even just getting everybody to copy'n'paste some code into some file is a hassle. And for what? Just so that you can say "don't worry about that part for now?".

But this is nowhere near fundamental or important enough to warrant polluting std .

I think by facilitating the teaching of the language it could do more good than harm. But I can understand your concern ... thanks for explaining your disagreement :slight_smile:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.