It might seem a bit early to talk about those topics but it is a good spot for Rust because these features are often used while writing initial code. We will also talk about error handling, which you will encounter sooner or later as it is quite different from most of today’s programming languages.
This chapter will not discuss things like debugging running executables (e.g., with Valgrind).
Okay so first, Rust packs a built-in testing/benching framework – an approach that might seem familiar to people coming from Ruby.
In order to use it, you will need to preappend the #[test]
compiler attribute (notice the # and brackets syntax) to a function that will serve as test.
Here is one:
#[test]
fn try_fetch() {
if external::input() == -1 {
fail!("Couldn't retrieve the result!");
}
}
These functions – just like blocks or statements marked with the #[cfg(test)]
compiler attribute – will not be part of the default compiled code. This way, you can keep your files tidy by placing the tests relating to a specific part of your program at the bottom of the corresponding file.
Test functions are blackboxes: they have no arguments nor returns.
Besides fail!
that halts the program and displays the error message passed as argument, tests are often made of assertions: the assert!
macro takes a boolean expression as parameter; it does nothing if it evaluates to true and fails when it’s false. assert_eq!
is a variant takes 2 variables as parameters and asserts equality.
use std::ascii::OwnedStrAsciiExt;
#[test]
fn lowercase_ascii() {
let tmp = ~"ME";
assert_eq!(~"me", tmp.into_ascii_lower());
}
Now, we just have to call rustc --test
with the name of our file, and run it.
That is how the output looks like (when everything is fine, that is):
running 1 test
test lowercase_ascii ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
There is also #[bench]
for benchmarking certain functions. These features are largely used in the Rust compiler itself to avoid any regression during the development process.
Note: if you are running your program in debugging mode (that is, with RUST_LOG
cmdline parameter defined to level 1/2/3/4), error!
warn!
info!
debug!
will get triggered depending on the logging level.
Rust does not have “opt-in” error handling (well, it trys to avoid opt-in safety measures when possible); that is, it works with the use of an Option
wrapper directly in the function declaration.
Here are its possible values as defined in an enum
(C-like cases enumerator):
enum Option<T> {
Some(T),
None
}
Note: if you are coming from C++, you will recognize those <>
which denote polymorphism, i.e. a method that applies to various variable types (T
being a lambda type). We will go back on those notions and on the trait
concept in a later chapter.
So, an Option
type encapsulating a certain type of variable will return Some()
if a value gets catched or None
if nothing passes through it.
Now let’s go about doing error handling, what we must do is destructuring those types so that we can make use of the value if we get one and also handle the case where we would not get anything returned.
If you are a C user, you are probably thinking about the use of the switch
pattern?
Well, Rust has a more powerful tool called match
, for pattern matching.
The mainline Rust documentation has a good example to show its most basic innards:
match my_number {
x if x < 0 => println!("something lower than zero"),
0 => println!("zero"),
1 | 2 => println!("one or two"),
3..10 => println!("three to ten"),
_ => println!("something else")
}
Something that must be noted is that match
must handle all possible cases for it to compile.
The wildcard _
means: “for all (other) possible cases”, so you will often have to use it as the last option, unless you are, for example, matching over all the individual components of an enum
.
Here is how we would match against an Option
value:
match optional_arg {
Some(val) => val,
None => () // `nil`, the empty type
}
If you remember, about assignments:
return
statementWe said that match
was not the only way to destructure an Option
type before, and there is another way involving Rust’s functional capatibilities.
let optional_arg: Option<int> = get_option_val();
// this will remove the `Option` wrapper,
// i.e. return the value contained in `Some()` or fail
optional_arg.unwrap();
// this will unwrap or fail with the specified message in the `None` case
optional_arg.expect("No value returned!");
// this will return the value passed as parameter when encountering `None`
optional_arg.unwrap_or(3);
Note: as you can see, error handling is not a big deal in the sense that you can simply call an .unwrap()
if you do not care much about the failing case.
This does not give you as much possibilities as with match
but it covers the common use case of Option
. There is also .is_some()
or .is_none()
if you want to do conditionals.
Result
sThere is a variant to the Option
type, called Result
:
enum Result<T, E> {
Ok(T),
Err(E)
}
It is quite different from Option
:
Option
…Result
will handle cases when computation errors can occur (and even allow you to do different things depending on the error message) while Option
handles cases when an argument can be missing, it’s also used to pass NULL pointers to C, and so on…Similarly to Option
, Result
has the is_ok()
and is_err()
methods.
match
it?Pattern matching is pretty powerful, noticeably when coupled with the use of tuples.
Like in Python, tuples are heterogenous lists of elements (that is, with different, unsequenced element types).
Tuples come also in handy, for example, when returning multiple values from a function:
fn main() {
// type is ambiguous here and needs to be annotated
let f: f32 = 1.0e-2;
let (mantissa, exponent, sign) = f.integer_decode();
println!("{}", mantissa);
}
As an example, here is an implementation of FizzBuzz using pattern matching:
Print numbers from 1 to 100, but for multiples of three print “Fizz” instead and for the multiples of five print “Buzz”; for multiples of both print “FizzBuzz”.
fn main() {
for i in range(0u, 101) {
match (i % 3 == 0, i % 5 == 0) {
(true, true) => println!("Fizz Buzz"),
(true, false) => println!("Fizz"),
(false, true) => println!("Buzz"),
(false, false) => println!("{}", i)
}
}
}
If you want to see more on this, there is a whole article about various ways to implement this program in Rust.
Note: you can match through ref x
instead of x
to get a reference instead of a value (i.e. get a pointer referencing x
) – some functions will need referencing.