#value #monads #optional #error #pattern #nothing #dubious

perhaps

Maybe monad implementation with a more intuitive name. Using Certain and Dubious instead of Just and Nothing

3 stable releases

2.0.0 Sep 3, 2024
1.0.1 Sep 3, 2024

#1206 in Rust patterns

MIT/Apache

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

Perhapss 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

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 message
  • unwrap panics with a generic message
  • unwrap_or returns the provided default value
  • unwrap_or_default returns the default value of the type T (which must implement the Default trait)
  • unwrap_or_else returns the result of evaluating the provided function

Transforming contained values

These methods transform Perhaps to Result:

These methods transform the Certain variant:

These methods transform Perhaps<T> to a value of a possibly different type U:

  • map_or applies the provided function to the contained value of Certain, or returns the provided default value if the Perhaps is Dubious
  • map_or_else applies the provided function to the contained value of Certain, or returns the result of evaluating the provided fallback function if the Perhaps is Dubious

These methods combine the Certain variants of two Perhaps values:

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 the Perhaps and produces the contained value
  • iter produces an immutable reference of type &T to the contained value
  • iter_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>:

These methods transfer ownership of the contained value of an Perhaps:

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 :("),
}

No runtime deps