perhaps 2.0.0

Maybe monad implementation with a more intuitive name. Using Certain and Dubious instead of Just and Nothing
Documentation
  • Coverage
  • 100%
    42 out of 42 items documented39 out of 40 items with examples
  • Size
  • Source code size: 61.18 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 3.25 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Links
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • BrilliantSnow

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(...)][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:

# #![allow(dead_code)]
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:

# #![allow(dead_code)]
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]:

  • ok_or transforms [Certain(v)] to Ok(v), and [Dubious] to Err(err) using the provided default err value
  • ok_or_else transforms [Certain(v)] to Ok(v), and [Dubious] to a value of [Err] using the provided function
  • transpose transposes an [Perhaps] of a [Result] into a [Result] of an [Perhaps]

These methods transform the [Certain] variant:

  • filter calls the provided predicate function on the contained value t if the [Perhaps] is Self::Certain(t), and returns [Certain(t)] if the function returns true; otherwise, returns [Dubious]
  • flatten removes one level of nesting from an [Perhaps<Perhaps<T>>]
  • map transforms [Perhaps<T>] to [Perhaps<U>] by applying the provided function to the contained value of [Certain] and leaving [Dubious] 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 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:

  • zip returns Self::Certain((s, o)) if self is [Certain(s)] and the provided [Perhaps] value is [Certain(o)]; otherwise, returns [Dubious]
  • zip_with calls the provided function f and returns Self::Certain(f(s, o)) if self is [Certain(s)] and the provided [Perhaps] value is [Certain(o)]; otherwise, returns [Dubious]

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].

# use std::collections::BTreeMap;
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.

# use std::iter::{empty, once};
// 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][Iterator::product] and [sum][Iterator::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 contents
  • get_or_insert gets the current value, inserting a provided default value if it is [Dubious]
  • get_or_insert_default gets the current value, inserting the default value of type T (which must implement [Default]) if it is [Dubious]
  • get_or_insert_with gets the current value, inserting a default computed by the provided function if it is [Dubious]

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

  • take takes ownership of the contained value of an [Perhaps], if any, replacing the [Perhaps] with [Dubious]
  • replace takes ownership of the contained value of an [Perhaps], if any, replacing the [Perhaps] with a [Certain] 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 :("),
}