22
loading...
This website collects cookies to deliver better user experience
Option
, Result
and creating and using errors derived from std::error::Error
. But both Option
and Result
have a host of methods that I wanted to explore and describe in today's post. Below is an overview of some of the Option
methods I want to talk about:Method | Use Description | Return Type |
---|---|---|
and | Testing two Option s are not None
|
Option<U> |
and_then | Chaining Option s |
Option<U> |
expect | Panic if None | T |
filter | Filter the Option with a predicate |
Option<T> |
flatten | Removes nested Option s |
Option<T> |
is_none | Test the Option type |
bool |
is_some | Test the Option type |
bool |
iter | Iterate over its single or no value | an iterator |
iter_mut | Iterate over its single or no value | an iterator |
map | Transform the value into another | Option<U> |
map_or | Transform the value into another | U |
map_or_else | Transform the value into another | U |
ok_or | Transform the Option to a Result
|
Result |
ok_or_else | Transform the Option to a Result
|
Result |
or | Provide a new value if None
|
Option<T> |
or_else | Provide a value if None
|
Option<T> |
replace | Change the value to a Some while returning previous value |
Option<T> |
take | Change the value to None while returning original |
Option<T> |
transpose | Change Option of Result to Result of Option
|
Result, E> |
unwrap | Extract value | T |
unwrap_or | Extract value | T |
unwrap_or_default | Extract value | T |
unwrap_or_else | Extract value | T |
xor | Return one of the contained values | Option<T> |
zip | Merge Options
|
Option<(T, U)> |
Result
methods I want to talk about:Method | Use Description | Return Type |
---|---|---|
and | Testing two Results are not errors |
Result |
and_then | Chaining Results
|
Result |
err | Extract the error | Option<E> |
expect | Panic if Err
|
T |
expect_err | Panic if Ok
|
E |
is_err | Test the Result type |
bool |
is_ok | Test the Result type |
bool |
iter | Iterate over its single or no value | an iterator |
iter_mut | Iterate over its single or no vlaue | an iterator |
map | Transform the value into another | Result |
map_err | Transform the value into another | Result |
map_or | Transform the value into another | U |
map_or_else | Transform the value into another | U |
ok | Converts Result into Option
|
Option<T> |
or | Provide a new Result if Err
|
Result |
or_else | Provide a new Result if Err
|
Result |
transpose | Change Result of Option to Option of Result
|
Option<Result<T,E>> |
unwrap | Extract value | T |
unwrap_err | Extract error | E |
unwrap_or | Extract value | T |
unwrap_or_default | Extract value | T |
unwrap_or_else | Extract value | T |
Option
and Result
have similar methods and act in similar ways. This is not surprising if you think about it because both can return results or a non-result. In Option
's case, the non-result is an absence of a result (None
) and in Result
's case, the non-result is an error.and
, or
and in Option
's case, xor
.map
family of methods and extract values via the unwrap
methods.or
versus or_else
. The difference between these functions is how they provide a default value. The or
signifies that if there is no result (i.e. None
or Err
), then here is one I provide for you! The else
part signifies that I will provide the default value, but via a function. This allows a default result to be provided lazily. Passing a value to an or
type method will mean it is evaluated regardless of the original value. This is because all parameters in Rust are evaluated before or
is called. Because or_else
uses a function to provide the default value, this means it is not evaluated unless the original value is an None
or Err
.Option
and Result
generically as they share so much in common, I will use different terminology for the types of values they can have.Some
and Ok
values, I will call them good values.None
and Err
values, I will call them bad values.unwrap
family of methods. How they differ is what happens when the value is not good. If it is a good value, they just convert the value into the contained value. That is, an Option<T>
or Result<T,E>
becomes a T
.unwrap
. I wouldn't recommend using this as panicking is so user unfriendly, but it's good for prototype work and for during development.unwrap_or
and unwrap_or_else
do this and I've already explained what or
and or_else
means above.Default
trait, use unwrap_or_default
.Result
has a couple more unwrapping methods for errors: err
and unwrap_err
. err
will return an Option<E>
value, which is None if no error occurred. unwrap_err
extracts the error value.let maybe_name: Option<String> = get_dog_name();
let name1: String = maybe_name.unwrap();
let name2: String = maybe_name.unwrap_or(String::from("Fido"));
let name3: String = maybe_name.unwrap_or_else(|| String::from("Fido"));
let name4: String = maybe_name.unwrap_or_default();
let maybe_file: Result<std::fs::File, std::io::Error> = File::open("foo.txt");
let file_error: std::io::Error = maybe_file.unwrap_err();
let maybe_file_error: Option<std::io::Error> = maybe_file.err();
Option
and Result
can be treated as containers that have one or zero values in it. By iterating over a single Ok
or Some
value, this opens up Option
s and Result
s to all the iterator functionality.iter
provides the &T
and iter_mut
provides the &mut T
on the "collection".Option
and Result
, removing the requirement to convert to an iterator first. This is the map
family of methods and for Option
there is filter
.map
calls a function that converts the good value of one type to a good value of another. The function moves the wrapped value into a function that returns a new one, which doesn't even have to share the same type. So it converts an Option<T>
to a Option<U>
or a Result<T,E>
to a Result<U,E>
. For bad values, it remains the bad value with no transformation.map_or
and map_or_else
extends map
's functionality by provided a default value in the case of a bad value. This follows the same or
and or_else
functionality as described above. However, the function you provide for the default in the Result
's case is provided with the error value.Result
has one more version for transforming errors. This is map_err
and is often used, for example, to adapt standard errors to your own.struct Dog {
name: String,
breed: Breed,
}
let maybe_name: Option<String> = get_dog_name();
let dog1: Option<Dog> = maybe_name.map(|dog_name| Dog {
name: dog_name,
breed: Breed::Labrador
});
let dog2 = maybe_name.map_or(
Dog {
name: String::from("Fido"),
breed: Breed::Labrador,
},
|dog_name| Dog {
name: dog_name,
breed: Breed::Labrador,
}
);
let dog3 = maybe_name.map_or_else(
|| Dog { // This lambda is passed an error argument in Result case.
name: String::from("Fido"),
breed: Breed::Labrador,
},
|dog_name| Dog {
name: dog_name,
breed: Breed::Labrador,
},
);
let maybe_file: Result<File, std::io::Error> = std::fs::File::open("foo.txt");
let new_file: Result<File, MyError> = maybe_file.map_err(|_error| Err(MyError::BadStuff));_
Option
provides is_some
and is_none
to quickly determine if it contains a good or bad value.Result
also provides is_ok
and is_err
for the same reasons.if let
and match
syntaxes.let maybe_name = get_dog_name();
match maybe_name {
Some(name) => Dog { name, breed: Breed::Labrador },
None => Dog::default(),
}
// instead of:
let dog = if maybe_name.is_some() {
// You still need to deconstruct the value here so the
// is_some check is redundant.
if let Some(name) = maybe_name {
Dog {
name,
breed: Breed::GermanShepherd,
}
} else {
Dog::default()
}
};
Option
and Result
provide and
and or
methods that combine two values according to their logical rules.and
, both values need to be good, otherwise the result is bad. Also, when all values are good, the result is the final good value. This makes and
a really good way of chaining operations if you don't care about the results of anything but the last operation. For example:// All these operations must be good
fn get_name(person: &Person) -> Option<String> { ... }
fn clone_person(person: &Person, name: String) -> Option<Person> { ... }
// We only create a new clone if the first clone has a name. For some
// reason unnamed clones are not allowed to be cloned again!
let new_person = get_name(old_person).and(clone_person(old_person, "Brad"));
// new_person will either be None or Some(Person)
and_then
. This allows you to chain operations together but passing the result of one to the function of the next.Result
. and_then
is perfect for this, because I only care about the first line and none of the intermediate data such as the open file:use std::result::Result;
use std::fs::File;
fn read_first_line(file: &mut File) -> Result<String> { ... }
let first_line: Result<String> = File::open("foo.txt").and_then(|file| {
read_first_line(&mut file)
});
or
method will check the first result and will return that result only if it is good. If it is bad, it will return the second result. This is useful for finding alternative operations. If one fails, then let's try the other.struct Disease { ... }
fn get_doctors_diagnosis() -> Option<Disease> { ... }
fn get_vets_diagnosis() -> Option<Disease> { ... }
// We're desperate! Let's ask a vet if the doctor doesn't know.
let disease: Option<Disease> = get_doctors_diagnosis().or(get_vets_diagnosis());
or
differs to and
in that all operations must wrap the same type. In the and
example, the first operation wrapped a File
, then resulte in a wrapped String
. This is possible due to the nature of the logical AND. As soon as one operation is bad, all results are bad. This is not so for logical OR. Any operation could be bad and we will still get a good result if at least one of them is good. This means that all wrapped values must be the same. In the example, all wrapped values were of type Disease
.Option
provides one more logical combination and that is the logical XOR operation. The result is good if and only if only one of the operations is good. You can either take this result or the other, but not a combination of both.and_then
and map
do very similar things. They can transform a wrapped value into another. In the example above, and_then
changed an Option<File>
into an Option<String>
. With the map
example, we changed an Option<String>
into an Option<Dog>
. But they do differ and I want to talk about how they differ.and_then
it returns a value of type Option<U>
, whereas map
returns a value of type U
. This means that and_then
can change a value from a good one to a bad one but map
cannot; once good always good. map
is purely for the transformation of a wrapped value to another value and even type but not goodness. That is, that transformation step cannot fail by becoming Err
in the case of Result
, or None
in the case of Option
. With and_then
the transformation can fail. The function passed can return None
or an Err
.match
:match opt {
Some(t) => Ok(t),
None => Err(MyError::new()),
}
ok_or
and ok_or_else
to provide the error if the option is None
:let res = opt.ok_or(MyError::new());
let res = opt.ok_or_else(|| MyError::new());
match
:match res {
Ok(t) => Some(t),
Err(_) => None,
}
Result
uses a method ok
to do the conversion:let opt = res.ok();
Result
and Option
are container types that wrap a value of type T
. But that type T
can just as well be a Result
and an Option
too.Result
and Option
in a nested type. So, for example, a Result<Option<T>>
becomes a Option<Result<T>>
or vice versa. Both Result
and Option
offer a transpose
method to do that swapping.Result<Result<T,E>,E>
into a Result<T,E>
, or convert an Option<Option<T>>
into an Option<T>
. This operation is called flattening and both methods offer a flatten
method to do so. But, as of writing, Result::flatten
is only available in nightly builds and so therefore, it has not been stablised yet. This is why this article only shows flatten
in the Option
table at the beginning.Option
.replace
and take
are used to move values in and out of the Option
. replace
transfers ownership of a value into the Option
and returns the old value so something else can own it. This is useful to make sure the option is not in a valid state. You cannot just move its value out without replacing it with a new value. The borrow checker will not let you do that. Quite often this is used for options within structures.None
as you extract the value you want. That is, you want to do something like this:let mut x = Some(42);
// Extract the 42 to another owner, but now make x own `None`.
let extracted_value = x.replace(None)
replace
takes a value of type T
, meaning you can only replace it with a Some
variant. Option
provides another method take
to do this:let mut x = Some(42);
let extracted_value = x.take(); // x will be None after this.
Option
and Result
effectively over the past two articles. It's a lot to take in and I will certainly be referring to my own articles to help me remember how I should use them.