A read!() macro for std::io?

Proposal:

Currently, in order to read input, you have to go through a (subjectively) ugly set of motions; this new feature proposes a fix to that.

// Before
let mut input = String::new();
stdin().read_to_string(&mut input).unwrap();
// Remove mutability
let input = format!("{:.12}", input);
// After
let input = read!("{:.12}");

And let's say, you want to prompt the user for what you need them to input, then it becomes even worse; and because of this, I would also suggest a prompt! macro.

// Before
print!("Input your magic number: ");
stdout().flush().unwrap();

let mut input = String::new();
stdin().read_to_string(&mut input).unwrap();
// Remove mutability
let input = format!("{:.12}", input);

// ...

// After

let input = prompt!("Input your magic numbers" => "{:.12}");

// ...

This was all inspired by the text_io crate. I'm unsure how big the demand for this feature would be, but I saw it, and thought that it provided what Rust is supposed to be right after rock-solid-stable; high-level. This provides a high level abstraction to console I/O.

Problems:

Currently, this does not give you the choice of whether you should unwrap, expect or handle each error individually via a match statement; thus, this would damage the idea of zero-cost abstractions, and it would also damage Rust's idea of bringing each error to the foreground. A straightforward solution would be to just simply return a Result enum

You might be interested in the conversation in

3 Likes

Yeah, this topic comes up every once in a while. However, most of the time it's more a "I'd like to do X" with some amount of magic syntax and wishful thinking how things could be. But actual difficulty is to craft an API that works well for a wide variety of use cases.

I think most of us want this feature, or some variation of it. If you want to be the chosen one that brings us this gift, I suggest doing the following:

  • Read up the existing discussions on the subject
  • Implement a few prototypes and ideas as library. No compiler support is required for this feature, so there's no need for it being in std initially.
  • Ideally, your library is so good that it gains traction and becomes the go-to tool for the use cases it aims to solve
  • Do an RFC that proposes to lift the most commonly used features of your library into std. This should come with a great section analyzing other libraries on that subject, comparing benefits and tradeoffs.

Two more remarks: 1. If you're aiming for getting into std, keep the API purely read-only, i.e. don't do a "prompt"-style API. 2. I estimate the probability of this ever landing in std to be rather low. So your time is better spent trying to build a good crate that everybody uses than to get this into std. Think of std as a bonus, and not as the goal.

See also the recent

1 Like

At large, I have a knee-jerk reaction to STDIN-based text prompts, and have a predisposition against putting things in the standard library that aim to make this style of program easier to write. However, for the sake of this discussion I will try to avoid discussing their (de-)merits.

I will agree that read_to_string's mutable argument makes it annoying to use. However, it's also the wrong tool for the job here, because it reads to EOF. At the very least this should be BufRead::read_line, but you can solve the whole mutability issue by using BufRead::lines instead.

What I find most puzzling and suspect here is that your primary motivation for introducing a macro seems to be to have a place to put this "{:.12}". But why would you want to pad a string with spaces at the time you are reading it, rather then at the time it is used? If you merely wanted to remove mutability, let input = input; would suffice.

2 Likes

{:.12} does not add padding; it limits the amount of characters in a text. It starts with a . because it's typically used to limit the precision of decimal numbers to a certain value. 3.141592{:.4}3.1416, but it also works on strings.

And you are right, let input = input; would be enough, if you didn't also want to remove excess characters (I tried String::with_capacity(12), it didn't work). But it's also ugly :slight_smile:. I guess some of the blame would partially fall on me for not also specifying that it not only removes mutability, but also achieves the desired formatting.

format!("{:.12}", "This text exceeds the character limit!")

// Out: "This text ex"

Indeed, I misinterpreted the format string, but at the same time I feel like this goes to show just how uncommon I've found the need to use a precision specifier on a string type (or really, anything other than a float type) in rust. So it seems strange to see it being given front and center treatment here.

I'm not even sure how the precision specifier treats Unicode. Counts chars (i.e. scalar values), probably? If I really wanted a limit, I'd more likely impose a byte count (and error on excess), or maybe use the Unicode width crate if it is for display on a console (and truncate the excess).

And how is truncating the string to a specific length useful? I assume if you want to forbid inputs that are longer than 12 chars, you should show an error instead of silently truncating the string.

I think there's a misunderstanding: String::with_capacity(12) reserves 12 bytes, whereas "{.12}" truncates to 12 chars (Unicode code points), so the string can be up to 48 bytes.


If this is meant as an optimization and you want to stop reading from stdin after a certain number of bytes, you can do something like

use std::io::{self, BufRead, Read};

fn read_line_or_bytes(len: usize) -> Result<String, io::Error> {
    io::BufReader::with_capacity(len, io::stdin())
        .take(len.try_into().unwrap())
        .lines()
        .next()
        .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "line missing"))?
}

fn main() {
    println!("{}", read_line_or_bytes(12).unwrap());
}
1 Like

Interesting I would have thought it would limit of to a number of graphemes bit it doesn't.

Yeah, the standard library has only limited Unicode support. It supports text encoding, some character properties and case conversion, but anything beyond that (normalisation, segmentation, collation, ...) requires additional crates.

Grapheme based segmentation requires unicode database which might be too big to be embedded in every binaries depend on libstd.

Ah fair enough.

Good input, but this kind of defeats the entire purpose of not being ugly... I think that might actually be even worse!

It's verbose (I wouldn't say ugly) because this is a very uncommon operation. I don't remember ever reading code that does this, and I honestly don't understand why you want to do this. If you just read a line without enforcing a maximum string length, the code becomes much easier.

And if it's not beautiful enough for you, you can always write a helper function or a macro yourself.