21
AUG2014
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)
-
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.
-
Thanks!