3 stable releases
2.0.0 | Sep 3, 2024 |
---|---|
1.0.1 | Sep 3, 2024 |
#1206 in Rust patterns
62KB
390 lines
Optional values.
Type Perhaps
represents an optional value: every Perhaps
is either Certain
and contains a value, or Dubious
, and
does not. Perhaps
types are very common in Rust code, as
they have a number of uses:
- Initial values
- Return values for functions that are not defined over their entire input range (partial functions)
- Return value for otherwise reporting simple errors, where
Dubious
is returned on error - Optional struct fields
- Struct fields that can be loaned or "taken"
- Optional function arguments
- Nullable pointers
- Swapping things out of difficult situations
Perhaps
s are commonly paired with pattern matching to query the presence
of a value and take action, always accounting for the Dubious
case.
fn divide(numerator: f64, denominator: f64) -> Perhaps<f64> {
if denominator == 0.0 {
Dubious
} else {
Certain(numerator / denominator)
}
}
// The return value of the function is an option
let result = divide(2.0, 3.0);
// Pattern match to retrieve the value
match result {
// The division was valid
Certain(x) => println!("Result: {x}"),
// The division was invalid
Dubious => println!("Cannot divide by 0"),
}
Options and pointers ("nullable" pointers)
Rust's pointer types must always point to a valid location; there are
no "null" references. Instead, Rust has optional pointers, like
the optional owned box, [Perhaps]<Box<T>>
.
The following example uses Perhaps
to create an optional box of
[i32
]. Notice that in order to use the inner [i32
] value, the
check_optional
function first needs to use pattern matching to
determine whether the box has a value (i.e., it is Self::Certain(...)
) or
not (Dubious
).
let optional = Dubious;
check_optional(optional);
let optional = Certain(Box::new(9000));
check_optional(optional);
fn check_optional(optional: Perhaps<Box<i32>>) {
match optional {
Certain(p) => println!("has value {p}"),
Dubious => println!("has no value"),
}
}
The question mark operator, ?
Similar to the Result
type, when writing code that calls many functions that return the
Perhaps
type, handling Certain
/Dubious
can be tedious. The question mark
operator, ?
, hides some of the boilerplate of propagating values
up the call stack.
It replaces this:
fn add_last_numbers(stack: &mut Vec<i32>) -> Perhaps<i32> {
let a = stack.pop();
let b = stack.pop();
match (a, b) {
(Self::Certain(x), Self::Certain(y)) => Certain(x + y),
_ => Dubious,
}
}
With this:
fn add_last_numbers(stack: &mut Vec<i32>) -> Perhaps<i32> {
Certain(stack.pop()? + stack.pop()?)
}
It's much nicer!
Ending the expression with ?
will result in the Certain
's unwrapped value, unless the
result is Self::Dubious
, in which case Dubious
is returned early from the enclosing function.
?
can be used in functions that return Perhaps
because of the
early return of Dubious
that it provides.
Representation
Rust guarantees to optimize the following types T
such that
Perhaps<T>
has the same size, alignment, and function call ABI as T
. In some
of these cases, Rust further guarantees that
transmute::<_, Perhaps<T>>([0u8; size_of::<T>()])
is sound and
produces Perhaps::<T>::Dubious
. These cases are identified by the
second column:
T |
transmute::<_, Perhaps<T>>([0u8; size_of::<T>()]) sound? |
---|---|
Box<U> (specifically, only Box<U, Global> ) |
when U: Sized |
&U |
when U: Sized |
&mut U |
when U: Sized |
fn , extern "C" fn [^extern_fn] |
always |
num::NonZero* |
always |
ptr::NonNull<U> |
when U: Sized |
#[repr(transparent)] struct around one of the types in this list. |
when it holds for the inner type |
[^extern_fn]: this remains true for any argument/return types and any other ABI: extern "abi" fn
(e.g., extern "system" fn
)
This is called the "null pointer optimization" or NPO.
It is further guaranteed that, for the cases above, one can
mem::transmute
from all valid values of T
to Perhaps<T>
and
from Certain::<T>(_)
to T
(but transmuting Dubious::<T>
to T
is undefined behaviour).
Method overview
In addition to working with pattern matching, Perhaps
provides a wide
variety of different methods.
Querying the variant
The is_certain
and is_dubious
methods return true
if the Perhaps
is Certain
or Dubious
, respectively.
Adapters for working with references
as_ref
converts from&[Perhaps]<T>
to[Perhaps]<&T>
as_mut
converts from&mut [Perhaps]<T>
to[Perhaps]<&mut T>
as_deref
converts from&[Perhaps]<T>
to[Perhaps]<&T::Target>
as_deref_mut
converts from&mut [Perhaps]<T>
to[Perhaps]<&mut T::Target>
as_pin_ref
converts from[Pin]<&[Perhaps]<T>>
to[Perhaps]<[Pin]<&T>>
as_pin_mut
converts from[Pin]<&mut [Perhaps]<T>>
to[Perhaps]<[Pin]<&mut T>>
Extracting the contained value
These methods extract the contained value in an Perhaps<T>
when it
is the Certain
variant. If the Perhaps
is Dubious
:
expect
panics with a provided custom messageunwrap
panics with a generic messageunwrap_or
returns the provided default valueunwrap_or_default
returns the default value of the typeT
(which must implement theDefault
trait)unwrap_or_else
returns the result of evaluating the provided function
Transforming contained values
These methods transform Perhaps
to Result
:
ok_or
transforms [Certain(v)
] toOk(v)
, andDubious
toErr(err)
using the provided defaulterr
valueok_or_else
transforms [Certain(v)
] toOk(v)
, andDubious
to a value of [Err
] using the provided functiontranspose
transposes anPerhaps
of aResult
into aResult
of anPerhaps
These methods transform the Certain
variant:
filter
calls the provided predicate function on the contained valuet
if thePerhaps
isSelf::Certain(t)
, and returns [Certain(t)
] if the function returnstrue
; otherwise, returnsDubious
flatten
removes one level of nesting from anPerhaps<Perhaps<T>>
map
transformsPerhaps<T>
toPerhaps<U>
by applying the provided function to the contained value ofCertain
and leavingDubious
values unchanged
These methods transform Perhaps<T>
to a value of a possibly
different type U
:
map_or
applies the provided function to the contained value ofCertain
, or returns the provided default value if thePerhaps
isDubious
map_or_else
applies the provided function to the contained value ofCertain
, or returns the result of evaluating the provided fallback function if thePerhaps
isDubious
These methods combine the Certain
variants of two Perhaps
values:
zip
returnsSelf::Certain((s, o))
ifself
is [Certain(s)
] and the providedPerhaps
value is [Certain(o)
]; otherwise, returnsDubious
zip_with
calls the provided functionf
and returnsSelf::Certain(f(s, o))
ifself
is [Certain(s)
] and the providedPerhaps
value is [Certain(o)
]; otherwise, returnsDubious
Boolean operators
These methods treat the Perhaps
as a boolean value, where Certain
acts like true
and Dubious
acts like false
. There are two
categories of these methods: ones that take an Perhaps
as input, and
ones that take a function as input (to be lazily evaluated).
The and
, or
, and xor
methods take another Perhaps
as
input, and produce an Perhaps
as output. Only the and
method can
produce an Perhaps<U>
value having a different inner type U
than
Perhaps<T>
.
method | self | input | output |
---|---|---|---|
and |
Self::Dubious |
(ignored) | Dubious |
and |
Certain(x) |
Self::Dubious |
Dubious |
and |
Self::Certain(x) |
Self::Certain(y) |
Certain(y) |
or |
Self::Dubious |
Self::Dubious |
Dubious |
or |
Dubious |
Self::Certain(y) |
Certain(y) |
or |
Self::Certain(x) |
(ignored) | Certain(x) |
xor |
Self::Dubious |
Self::Dubious |
Dubious |
xor |
Dubious |
Self::Certain(y) |
Certain(y) |
xor |
Self::Certain(x) |
Dubious |
Certain(x) |
xor |
Self::Certain(x) |
Certain(y) |
Dubious |
The and_then
and or_else
methods take a function as input, and
only evaluate the function when they need to produce a new value. Only
the and_then
method can produce an Perhaps<U>
value having a
different inner type U
than Perhaps<T>
.
method | self | function input | function result | output |
---|---|---|---|---|
and_then |
Self::Dubious |
(not provided) | (not evaluated) | Dubious |
and_then |
Certain(x) |
x |
Self::Dubious |
Dubious |
and_then |
Self::Certain(x) |
x |
Self::Certain(y) |
Certain(y) |
or_else |
Self::Dubious |
(not provided) | Self::Dubious |
Dubious |
or_else |
Dubious |
(not provided) | Self::Certain(y) |
Certain(y) |
or_else |
Self::Certain(x) |
(not provided) | (not evaluated) | Certain(x) |
This is an example of using methods like and_then
and or
in a
pipeline of method calls. Early stages of the pipeline pass failure
values (Dubious
) through unchanged, and continue processing on
success values (Certain
). Toward the end, or
substitutes an error
message if it receives Dubious
.
let mut bt = BTreeMap::new();
bt.insert(20u8, "foo");
bt.insert(42u8, "bar");
let res = [0u8, 1, 11, 200, 22]
.into_iter()
.map(|x| {
// `checked_sub()` returns `Dubious` on error
x.checked_sub(1)
// same with `checked_mul()`
.and_then(|x| x.checked_mul(2))
// `BTreeMap::get` returns `Dubious` on error
.and_then(|x| bt.get(&x))
// Substitute an error message if we have `Dubious` so far
.or(Certain(&"error!"))
.copied()
// Won't panic because we unconditionally used `Certain` above
.unwrap()
})
.collect::<Vec<_>>();
assert_eq!(res, ["error!", "error!", "foo", "error!", "bar"]);
Comparison operators
If T
implements PartialOrd
then Perhaps<T>
will derive its
PartialOrd
implementation. With this order, Dubious
compares as
less than any Self::Certain
, and two Certain
compare the same way as their
contained values would in T
. If T
also implements
[Ord
], then so does Perhaps<T>
.
assert!(Dubious < Certain(0));
assert!(Self::Certain(0) < Certain(1));
Iterating over Perhaps
An Perhaps
can be iterated over. This can be helpful if you need an
iterator that is conditionally empty. The iterator will either produce
a single value (when the Perhaps
is Certain
), or produce no values
(when the Perhaps
is Dubious
). For example, into_iter
acts like
once(v)
if the Perhaps
is [Certain(v)
], and like empty()
if
the Perhaps
is Dubious
.
Iterators over Perhaps<T>
come in three types:
into_iter
consumes thePerhaps
and produces the contained valueiter
produces an immutable reference of type&T
to the contained valueiter_mut
produces a mutable reference of type&mut T
to the contained value
An iterator over Perhaps
can be useful when chaining iterators, for
example, to conditionally insert items. (It's not always necessary to
explicitly call an iterator constructor: many Iterator
methods that
accept other iterators will also accept iterable types that implement
IntoIterator
, which includes Perhaps
.)
let yep = Certain(42);
let nope = Dubious;
// chain() already calls into_iter(), so we don't have to do so
let nums: Vec<i32> = (0..4).chain(yep).chain(4..8).collect();
assert_eq!(nums, [0, 1, 2, 3, 42, 4, 5, 6, 7]);
let nums: Vec<i32> = (0..4).chain(nope).chain(4..8).collect();
assert_eq!(nums, [0, 1, 2, 3, 4, 5, 6, 7]);
One reason to chain iterators in this way is that a function returning
impl Iterator
must have all possible return values be of the same
concrete type. Chaining an iterated Perhaps
can help with that.
fn make_iter(do_insert: bool) -> impl Iterator<Item = i32> {
// Explicit returns to illustrate return types matching
match do_insert {
true => return (0..4).chain(Certain(42)).chain(4..8),
false => return (0..4).chain(Dubious).chain(4..8),
}
}
println!("{:?}", make_iter(true).collect::<Vec<_>>());
println!("{:?}", make_iter(false).collect::<Vec<_>>());
If we try to do the same thing, but using once()
and empty()
,
we can't return impl Iterator
anymore because the concrete types of
the return values differ.
// This won't compile because all possible returns from the function
// must have the same concrete type.
fn make_iter(do_insert: bool) -> impl Iterator<Item = i32> {
// Explicit returns to illustrate return types not matching
match do_insert {
true => return (0..4).chain(once(42)).chain(4..8),
false => return (0..4).chain(empty()).chain(4..8),
}
}
Collecting into Perhaps
Perhaps
implements the FromIterator
trait,
which allows an iterator over Perhaps
values to be collected into an
Perhaps
of a collection of each contained value of the original
Perhaps
values, or Self::Dubious
if any of the elements was Dubious
.
let v = [Self::Certain(2), Self::Certain(4), Dubious, Certain(8)];
let res: Perhaps<Vec<_>> = v.into_iter().collect();
assert_eq!(res, Dubious);
let v = [Self::Certain(2), Self::Certain(4), Certain(8)];
let res: Perhaps<Vec<_>> = v.into_iter().collect();
assert_eq!(res, Certain(vec![2, 4, 8]));
Perhaps
also implements the Product
and
Sum
traits, allowing an iterator over Perhaps
values
to provide the product
and
sum
methods.
let v = [Dubious, Self::Certain(1), Self::Certain(2), Certain(3)];
let res: Perhaps<i32> = v.into_iter().sum();
assert_eq!(res, Dubious);
let v = [Self::Certain(1), Self::Certain(2), Certain(21)];
let res: Perhaps<i32> = v.into_iter().product();
assert_eq!(res, Certain(42));
Modifying an Perhaps
in-place
These methods return a mutable reference to the contained value of an
Perhaps<T>
:
insert
inserts a value, dropping any old contentsget_or_insert
gets the current value, inserting a provided default value if it isDubious
get_or_insert_default
gets the current value, inserting the default value of typeT
(which must implementDefault
) if it isDubious
get_or_insert_with
gets the current value, inserting a default computed by the provided function if it isDubious
These methods transfer ownership of the contained value of an
Perhaps
:
take
takes ownership of the contained value of anPerhaps
, if any, replacing thePerhaps
withDubious
replace
takes ownership of the contained value of anPerhaps
, if any, replacing thePerhaps
with aCertain
containing the provided value
Examples
Basic pattern matching on Perhaps
:
let msg = Certain("howdy");
// Take a reference to the contained string
if let Certain(m) = &msg {
println!("{}", *m);
}
// Remove the contained string, destroying the Perhaps
let unwrapped_msg = msg.unwrap_or("default message");
Initialize a result to Dubious
before a loop:
enum Kingdom { Plant(u32, &'static str), Animal(u32, &'static str) }
// A list of data to search through.
let all_the_big_things = [
Kingdom::Plant(250, "redwood"),
Kingdom::Plant(230, "noble fir"),
Kingdom::Plant(229, "sugar pine"),
Kingdom::Animal(25, "blue whale"),
Kingdom::Animal(19, "fin whale"),
Kingdom::Animal(15, "north pacific right whale"),
];
// We're going to search for the name of the biggest animal,
// but to start with we've just got `Dubious`.
let mut name_of_biggest_animal = Dubious;
let mut size_of_biggest_animal = 0;
for big_thing in &all_the_big_things {
match *big_thing {
Kingdom::Animal(size, name) if size > size_of_biggest_animal => {
// Now we've found the name of some big animal
size_of_biggest_animal = size;
name_of_biggest_animal = Certain(name);
}
Kingdom::Animal(..) | Kingdom::Plant(..) => ()
}
}
match name_of_biggest_animal {
Certain(name) => println!("the biggest animal is {name}"),
Dubious => println!("there are no animals :("),
}