CS 430: Lecture 11 - Rust Mains

Dear students:

Today we start talking about Rust.

Dealing with Option and Result

Rust is serious about forcing programmers to deal with errors. And it doesn't have exceptions. Instead functions give back Option or Result. Option is this enum:

enum Option<T> {
  None,
  Some(T),
}

Result is this enum:

enum Result<T, E> {
  Error(E),
  Ok(T),
}

Rust code that anticipates failure is going to be messier than the code that you've been writing in Java that doesn't anticipate failure. Let's examine the mechanisms we have for dealing with these failure types.

Our first and most verbose option is a match expression or statement. If we are trying to open a file, we choose between panic and the successfully opened file with this match expression:

let file = match File::open("names.txt") {
  Err(error) => panic!("Couldn't open names.txt. Cause: {}", error)),
  Ok(file) => file,
};

This code feels like it shouldn't compile because two arms of the match don't yield the same type. The Ok arm yields a file type, and the Err arm yields a…what? The panic! macro calls the exit function, which has a return type of ! or never. The type checker will overlook never types since they signal that a computation is not going to return back to the surrounding context.

If you want to respond to the Ok variant, you can write a conditional statement with a let-binding and pattern matching built into the condition:

if let Ok(file) = File::open("names.txt") {
  // read file
}

But let's be honest. These solutions require more code than we want to write and look at. If we want to fail or proceed with just a single statement, we may use the unwrap function, which has a definition like this:

impl Result<T> {
  fn unwrap(self) -> T {
    match self {
      Err(error) => panic!("some stock error message: {}", error),
      Ok(value) => value,
    }
  }
}

This function lets us much shorter code:

let file = File::open("names.txt").unwrap();

The expect function behaves similarly, but it lets us send in a custom error message:

let file = File::open("names.txt").expect("couldn't open names.txt");

Both unwrap and expect panic, offering no chance for recovery. There are some related functions that are less despairing. The unwrap_or function gives you a chance to pick a default value:

impl Result<T> {
  fn unwrap_or(self, default: T) -> T {
    match self {
      Err(_) => default,
      Ok(value) => value,
    }
  }
}

Command-Line Arguments

for arg in env::args().skip(1) {
    println!("{:?}", arg);
}
let mut args = env::args().skip(1);
let path = args.next().expect("Expected path as command-line argument.");
println!("path: {:?}", path);

Slurping Input

Rust doesn't have anything like Scanner in its standard library. There are some third-party crates that define macros that behave like scanf. Feel free to explore those on your own. For today, we'll confine ourselves to stock Rust and just read strings from a file.

If we want just one line, we might write this code:

let file = File::open("names.txt").expect("couldn't open names.txt");
let mut reader = BufReader::new(file);
let mut line = String::new();
reader.read_line(&mut line).expect("read failed");

If we want to process all the lines, we can use an iterator and a for loop:

let file = File::open("names.txt").expect("couldn't open names.txt");
let reader = BufReader::new(file);
for line in reader.lines() {
    println!("line: {:?}", line.unwrap());
}

If we want all the text, we can us std::fs::read_to_string:

let text = fs::read_to_string("names.txt").expect("read failed");
println!("{}", text);

Reading from STDIN is a little different, as we'll see in a moment.

Parsing Strings

When you read from a file or command-line arguments, you receive a string. If you expect the content to actually be some other type, you need to parse the string, which you can do with the parse method of str. There are many definitions of this method for various types, and you must indicate which one you want. One way is to include a specific generic parameter on the function call using the turbofish:

let ago_text = "87";
let ago = ago_text.parse::<u32>();

Another way is to explicitly state the variable type. But parsing may fail if the content isn't what you think it is. You'll either need to specify a Result type or unwrap the value:

let ago_text = "87";
let ago: Result<u32> = ago_text.parse();
let ago: u32 = ago_text.parse().expect("bad number");

Exercises

We'll explore these failure-aware types and file I/O further by solving these problems from Open Kattis:

TODO

Here's your list of things to do in the near future:

Commence work on project 3 in Rust. Post your first progress report by Friday noon.
The textbook server is having troubles that I haven't diagnosed yet. If you didn't get your reading exercises completed, you now have until Thursday at 9 AM.

See you next time.

Sincerely,

P.S. It's time for a haiku!

Goldilocks tried C… Java, Python, Haskell, Rust… She'll need more bears soon