Rusting

The section where I examine what I am learning when compiling Rust code.

21

AUG
2014

Guard Clauses, Rust Style

When I'm programming in Ruby, I will often use guard clauses to prevent undesirable scenarios. For example, let's say I'm building a simple Stack:

class Stack
  def initialize
    @numbers = [ ]
  end

  def push(number)
    @numbers.push(number)
  end

  def peek
    fail "Stack underflow" if @numbers.empty?

    @numbers.last
  end
end

stack = Stack.new

ARGV.each do |number|
  stack.push(number.to_f)
end

p stack.peek

If I only want to work with numbers everywhere, I add a line like the call to fail() above. This prevents a nil from being returned from peek(), ruining my expectation that I will have numbers everywhere.

When I first started playing with Rust, I wanted to write code the same way:

use std::os;

struct Stack {
    numbers: Vec<f64>
}
impl Stack {
    fn new() -> Stack {
        Stack{numbers: vec![]}
    }

    fn push(&mut self, number: f64) {
        self.numbers.push(number);
    }

    fn peek(&self) -> f64 {
        if self.numbers.is_empty() { fail!("Stack underflow"); }

        self.numbers.last()
    }
}

fn main() {
    let mut stack = Stack::new();

    for number in os::args().tail().iter() {
        stack.push(from_str(number.as_slice()).expect("Not a number"));
    }

    println!("{}", stack.peek());
}

But this code doesn't compile:

$ rustc guards.rs
guards.rs:18:9: 18:28 error: mismatched types: expected `f64` but found `core::option::Option<&f64>` (expected f64 but found enum core::option::Option)
guards.rs:18         self.numbers.last()
                     ^~~~~~~~~~~~~~~~~~~
error: aborting due to previous error

The complaint from Rust here is that self.numbers.last() isn't actually going to return a float. Instead it's going to return an Option<&f64>. This is how Rust handles null values. Option is an enum that, in my case, will hold either Some(&f64) or None.

I've already convinced myself that I won't get that None I've been trying to avoid, because I used the guard clause. However, the compiler doesn't know what I know and it isn't yet convinced. To appease it, I need to enumerate the possible scenarios:

    fn peek(&self) -> f64 {
        if self.numbers.is_empty() { fail!("Stack underflow"); }

        match self.numbers.last() {
            Some(number) => { *number }
            None         => { 0.0  /* won't get here */ }

        }
    }

Here I use pattern matching to get at the number inside the Option and this does work:

$ ./guards 42
42
$ ./guards
task '<main>' failed at 'Stack underflow', guards.rs:22

Of course, my code is a little silly. I had to make up a dummy return value that I know will never be used but that the compiler requires for completeness.

The better thing to do here would be to realize that the Rust compiler is trying to show me how to do things the Rust way and adapt:

    fn peek(&self) -> f64 {
        match self.numbers.last() {
            Some(number) => { *number }
            None         => { fail!("Stack underflow") }

        }
    }

This is better Rust style for the same code, but it's still a little wordy. This pattern is very common in Rust so Option has an expect() method that will either return the Some(<T>) or fail!() with an error if there is None.

I actually already snuck this pattern into the code I showed you. Remember my main() function?

fn main() {
    let mut stack = Stack::new();

    for number in os::args().tail().iter() {
        stack.push(from_str(number.as_slice()).expect("Not a number"));
    }

    println!("{}", stack.peek());
}

It turns out that from_str() also returns an Option, because the string you pass may not be a valid number. When that's the case, you'll get a None. I sidestepped that issue here with a call to expect().

That means the complete and most Rust-like translation of the Ruby script we started with is:

use std::os;

struct Stack {
    numbers: Vec<f64>
}
impl Stack {
    fn new() -> Stack {
        Stack{numbers: vec![]}
    }

    fn push(&mut self, number: f64) {
        self.numbers.push(number);
    }

    fn peek(&self) -> f64 {
        *self.numbers.last().expect("Stack underflow")
    }
}

fn main() {
    let mut stack = Stack::new();

    for number in os::args().tail().iter() {
        stack.push(from_str(number.as_slice()).expect("Not a number"));
    }

    println!("{}", stack.peek());
}

With this code, both the compiler and I know I'm not dealing with non-numbers.

Comments (2)
  1. Avdi Grimm
    Avdi Grimm August 21st, 2014 Reply Link

    Lovely! I can't say I love the choice of the word expect() for that operation, but I'm sure I could get used to it.

    I have to say I love that this code looks like C++ with all the crap removed.

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
  2. Rusty Mutton
    Rusty Mutton August 21st, 2014 Reply Link

    Thanks!

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
Leave a Comment (using GitHub Flavored Markdown)

Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

Ajax loader