perhaps 1.0.0

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

Optional values.

Type [Perhaps] represents an optional value: every [Perhaps] is either Self::Certain and contains a value, or Self::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 Self::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 Self::Dubious case.

fn divide(numerator: f64, denominator: f64) -> Perhaps<f64> {
if denominator == 0.0 {
Self::Dubious
} else {
Self::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
Self::Certain(x) => println!("Result: {x}"),
// The division was invalid
Self::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 (Self::Dubious).

let optional = Self::Dubious;
check_optional(optional);

let optional = Self::Certain(Box::new(9000));
check_optional(optional);

fn check_optional(optional: Perhaps<Box<i32>>) {
match optional {
Self::Certain(p) => println!("has value {p}"),
Self::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 Self::Certain/Self::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)) => Self::Certain(x + y),
_ => Self::Dubious,
}
}

With this:

# #![allow(dead_code)]
fn add_last_numbers(stack: &mut Vec<i32>) -> Perhaps<i32> {
Self::Certain(stack.pop()? + stack.pop()?)
}

It's much nicer!

Ending the expression with ? will result in the Self::Certain's unwrapped value, unless the result is Self::Dubious, in which case Self::Dubious is returned early from the enclosing function.

? can be used in functions that return [Perhaps] because of the early return of Self::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>::Self::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 Self::Certain::<T>(_) to T (but transmuting Self::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_some and is_none methods return [true] if the [Perhaps] is Self::Certain or Self::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 Self::Certain variant. If the [Perhaps] is Self::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 Self::Certain variant:

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

These methods combine the Self::Certain variants of two [Perhaps] values:

Boolean operators

These methods treat the [Perhaps] as a boolean value, where Self::Certain acts like [true] and Self::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) Self::Dubious
and Self::Certain(x) Self::Dubious Self::Dubious
and Self::Certain(x) Self::Certain(y) Self::Certain(y)
or Self::Dubious Self::Dubious Self::Dubious
or Self::Dubious Self::Certain(y) Self::Certain(y)
or Self::Certain(x) (ignored) Self::Certain(x)
xor Self::Dubious Self::Dubious Self::Dubious
xor Self::Dubious Self::Certain(y) Self::Certain(y)
xor Self::Certain(x) Self::Dubious Self::Certain(x)
xor Self::Certain(x) Self::Certain(y) Self::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) Self::Dubious
and_then Self::Certain(x) x Self::Dubious Self::Dubious
and_then Self::Certain(x) x Self::Certain(y) Self::Certain(y)
or_else Self::Dubious (not provided) Self::Dubious Self::Dubious
or_else Self::Dubious (not provided) Self::Certain(y) Self::Certain(y)
or_else Self::Certain(x) (not provided) (not evaluated) Self::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 (Self::Dubious) through unchanged, and continue processing on success values (Self::Certain). Toward the end, or substitutes an error message if it receives Self::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 `Self::Dubious` on error
x.checked_sub(1)
// same with `checked_mul()`
.and_then(|x| x.checked_mul(2))
// `BTreeMap::get` returns `Self::Dubious` on error
.and_then(|x| bt.get(&x))
// Substitute an error message if we have `Self::Dubious` so far
.or(Self::Certain(&"error!"))
.copied()
// Won't panic because we unconditionally used `Self::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, Self::Dubious compares as less than any Self::Certain, and two Self::Certain compare the same way as their contained values would in T. If T also implements [Ord], then so does [Perhaps<T>].

assert!(Self::Dubious < Self::Certain(0));
assert!(Self::Certain(0) < Self::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 Self::Certain), or produce no values (when the [Perhaps] is Self::Dubious). For example, into_iter acts like once(v) if the [Perhaps] is Self::Certain(v), and like empty() if the [Perhaps] is Self::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 = Self::Certain(42);
let nope = Self::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(Self::Certain(42)).chain(4..8),
false => return (0..4).chain(Self::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 Self::Dubious.

let v = [Self::Certain(2), Self::Certain(4), Self::Dubious, Self::Certain(8)];
let res: Perhaps<Vec<_>> = v.into_iter().collect();
assert_eq!(res, Self::Dubious);
let v = [Self::Certain(2), Self::Certain(4), Self::Certain(8)];
let res: Perhaps<Vec<_>> = v.into_iter().collect();
assert_eq!(res, Self::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 = [Self::Dubious, Self::Certain(1), Self::Certain(2), Self::Certain(3)];
let res: Perhaps<i32> = v.into_iter().sum();
assert_eq!(res, Self::Dubious);
let v = [Self::Certain(1), Self::Certain(2), Self::Certain(21)];
let res: Perhaps<i32> = v.into_iter().product();
assert_eq!(res, Self::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]:

  • take takes ownership of the contained value of an [Perhaps], if any, replacing the [Perhaps] with Self::Dubious
  • replace takes ownership of the contained value of an [Perhaps], if any, replacing the [Perhaps] with a Self::Certain containing the provided value

Examples

Basic pattern matching on [Perhaps]:

let msg = Self::Certain("howdy");

// Take a reference to the contained string
if let Self::Certain(m) = &msg {
println!("{}", *m);
}

// Remove the contained string, destroying the Perhaps
let unwrapped_msg = msg.unwrap_or("default message");

Initialize a result to Self::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 `Self::Dubious`.
let mut name_of_biggest_animal = Self::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 = Self::Certain(name);
}
Kingdom::Animal(..) | Kingdom::Plant(..) => ()
}
}

match name_of_biggest_animal {
Self::Certain(name) => println!("the biggest animal is {name}"),
Self::Dubious => println!("there are no animals :("),
}