19
\$\begingroup\$

Wanna learn Rust? Ya rly!

I can haz FizzBuzz? No wai!

Gimme /bin/cat then. But rustc say LOL! Must handle teh err0rz!

Now iz all err0r handlurz, so /bin/lolcat. Rust sux, or OP sux?


Plain English translation:

Wanted to learn Rust, but didn't want to write FizzBuzz. How about writing /bin/cat? But for every function call, Rust insists that you do something with the Result, which may be an error. Simple task turned out to be complicated by all the error handling. What went wrong?


// Using rustc 1.0.0-beta.3
use std::env;
use std::io::Read;
use std::io::Write;
use std::fs::File;
use std::path::Path;
use std::process;

macro_rules! println_stderr(
    ($($arg:tt)*) => (
        match writeln!(&mut ::std::io::stderr(), $($arg)* ) {
            Err(e) => panic!("Unable to write to stderr: {}", e),
            Ok(_) => {},
        }
    )
);

fn main() {
    let args: Vec<_> = env::args().collect();
    if args.len() < 2 {
        println_stderr!("Usage: {} GIMME_TEH_FILEZ",
                Path::new(&args[0]).file_name().unwrap().to_str().unwrap());
        process::exit(1);
    }

    let path = Path::new(&args[1]);
    let mut file = match File::open(path) {
        Err(e) => {
            println_stderr!("{}", e);
            process::exit(1);
        },
        Ok(f) => f,
    };

    let mut buf = String::new();
    match file.read_to_string(&mut buf) {
        Err(e) => {
            println_stderr!("{}", e);
            process::exit(1);
        },
        Ok(_) => {},
    }
    print!("{}", buf);
}
\$\endgroup\$
17
\$\begingroup\$
  • C would have allowed you to just ignore errors and pretend everything was good. A naïve cat would be very short, but rather unreliable also. A well‐written cat will go and use all the error codes that are returned to produce a useful result.

  • Python and similar languages would raise exceptions in these cases, leading to you getting tracebacks in case of errors on a naïve approach; again, a well‐written cat is going to handle all of these exceptions manually, producing a neater result.

  • Rust makes error‐handling explicit and makes it harder for you to ignore errors, though you still can ignore them if you want by unwrapping results, which gets you something more like the default arrangement in languages like Python. The explicit nature of error handling can be a pain in places (most notably for simple scripts), but it makes for a much more solid result in general as it scales very well.

One must realise that in these common tasks that people tend to assume must succeed there are actually possibilities of failure.

  • Writing to stderr and stdout may fail. They might be being piped to a read‐only file, or on a device that is not ready or full. print!, for example, will panic if writing fails.

  • By using String, you’ve constrained your lolcat to only use UTF‐8; if you pass a non‐UTF‐8 file, it will blow up in your face. You might want to vec![] instead of String::new(), read_to_end instead of read_to_string and stdout().write(&buf) instead of print!("{}", buf).

Macros can be quite good at alleviating these sorts of pains; there is try!, for example, which works well at handling Results; if you refactored your code so that all of the important stuff was inside a function which could then return Result<(), io::Error>, for example, you could use try! on all of the fallible I/O operations. As it is, you can do things like writing a new macro to assist, and also give a more helpful message explaining where the error occurred.

Here’s something more like what I would write; of course, there are various points that are subjective, and I have made some stylistic changes to be in line with what I believe to be more common standard style:

use std::env;
use std::io::{Read, Write, stdout};
use std::fs::File;
use std::path::Path;
use std::process;

macro_rules! println_stderr {
    ($($arg:tt)*) => {
        match writeln!(&mut ::std::io::stderr(), $($arg)*) {
            Err(e) => panic!("Unable to write to stderr: {}", e),
            Ok(_) => (),
        }
    }
}

macro_rules! try_or_die {
    ($e:expr, $err_msg:expr) => {
        match $e {
            Ok(x) => x,
            Err(e) => {
                println_stderr!(concat!($err_msg, ": {}"), e);
                process::exit(1);
            },
        }
    }
}

fn main() {
    let args: Vec<_> = env::args().collect();
    if args.len() < 2 {
        println_stderr!("Usage: {} GIMME_TEH_FILEZ",
                        Path::new(&args[0]).file_name().unwrap().to_str().unwrap());
        process::exit(1);
    }

    let path = Path::new(&args[1]);
    let mut file = try_or_die!(File::open(path), "failed to open input file");

    let mut buf = vec![];
    try_or_die!(file.read_to_end(&mut buf), "failed to read input file");
    try_or_die!(stdout().write(&buf), "failed to write output");
}

(At first I had try_or_exit, but then try_or_die sounded better.)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.