rand_functors/
lib.rs

1//! `rand-functors` provides an abstraction over different ways of evaluating
2//! random processes expressed as functions of both deterministic and stochastic
3//! data. This is achieved using a combination of a type-based version of the
4//! Strategy pattern and functional programming's Functor pattern.
5//!
6//! A motivating problem for this crate is the code duplication present across
7//! these two functions modelling the same random process:
8//! ```
9//! use rand::random;
10//!
11//! fn next_state(mut state: u8) -> u8 {
12//!     state = state.wrapping_add(random());
13//!     if random() {
14//!         state %= 3;
15//!     }
16//!     state
17//! }
18//!
19//! fn next_states(state: u8) -> Vec<u8> {
20//!     let mut out: Vec<_> = (0..=255).map(|r| state.wrapping_add(r)).collect();
21//!     out.append(&mut out.iter().copied().map(|i| i % 3).collect());
22//!     out
23//! }
24//! ```
25//! While these functions may appear different, the same random process is
26//! embedded in both of them. A random `u8` is added to `state` and then, if a
27//! random `bool` is `true`, the state will be set to itself modulo 3.
28//!
29//! This redundant implementation of the random process could pose issues during
30//! a refactor. If one decides to change the `%= 3` to a `%= 5` in `next_state`,
31//! he or she will need to make the corresponding update in `next_states`.
32//!
33//! Using `rand-functors`, these two functions can be combined as:
34//! ```
35//! use rand::rng;
36//! use rand_functors::{Functor, RandomStrategy};
37//!
38//! fn next_state<S: RandomStrategy>(state: u8) -> S::Functor<u8> {
39//!     let mut out = S::fmap_rand(Functor::pure(state), &mut rng(), |s, r| s.wrapping_add(r));
40//!     out = S::fmap_rand(out, &mut rng(), |s, r| if r { s % 3 } else { s });
41//!     out
42//! }
43//! ```
44//! This new implementation makes `next_state` generic over a [`RandomStrategy`]
45//! `S`. Its return type is also changed to the [`Functor`] associated with `S`.
46//! Inside, `state` is converted from `u8` to `S::Functor<u8>`. The remainder of
47//! the function is essentially the same as the original `next_state`, but each
48//! operation a random sample is now wrapped in a call to `S::fmap_rand`.
49//! Calling `next_state::<Sampler>(s)` would be equivalent to calling
50//! `next_state(s)` before. Similarly, one could call
51//! `next_state::<Enumerator>(s)` instead of using `next_states(s)`, which would
52//! require maintaining a separate implementation of the same core process.
53//!
54//! At present, `rand-functors` only supports random variables that are either
55//! of type [`bool`] or of a numeric type occupying no more than 16 bits by
56//! default. However, it is possible to implement all the requisite traits for a
57//! custom data type.
58
59#![cfg_attr(not(feature = "std"), no_std)]
60#![warn(clippy::cargo)]
61#![warn(missing_docs)]
62
63#[cfg(feature = "alloc")]
64extern crate alloc;
65
66pub use strategies::*;
67
68mod functors;
69mod random_variable_ranges;
70mod random_variables;
71mod strategies;
72
73use core::hash::Hash;
74
75use rand::distr::uniform::{SampleRange, SampleUniform};
76use rand::distr::StandardUniform;
77use rand::prelude::*;
78
79/// A strategy for evaluating sequences of functions of random data.
80///
81/// Types implementing `RandomStrategy` are typically not constructed. For this
82/// same reason, they are typically unit structs. Behaviour should be specified
83/// at compile-time, to allow calls to `fmap_rand` and `Functor::fmap` to be
84/// properly inlined.
85pub trait RandomStrategy {
86    /// The functor that this strategy operates on.
87    ///
88    /// Functions using a given strategy will typically return its associated
89    /// functor in the form `S::Functor<T>`.
90    type Functor<I: Inner>: Functor<I>;
91
92    /// Applies the given function to the functor's inner.
93    fn fmap<A: Inner, B: Inner, F: Fn(A) -> B>(f: Self::Functor<A>, func: F) -> Self::Functor<B>;
94
95    /// Using the strategy specified by the implementor, applies the given
96    /// binary function to the given functor and an element of the sample space
97    /// of a [`RandomVariable`].
98    ///
99    /// Note that **no guarantees** are made about whether or how the `rand`
100    /// parameter will be used. It may be sampled zero, one, or arbitrarily many
101    /// times. It may be used to sample values of type `R`, of type [`usize`],
102    /// or some other type. If some model of the random number generator is
103    /// available, then that model should be responsible for enumerating
104    /// possible outcomes.
105    fn fmap_rand<A: Inner, B: Inner, R: RandomVariable, F: Fn(A, R) -> B>(
106        f: Self::Functor<A>,
107        rng: &mut impl Rng,
108        func: F,
109    ) -> Self::Functor<B>
110    where
111        StandardUniform: Distribution<R>;
112
113    /// Using the strategy specified by the implementor, applies the given
114    /// binary function to the given functor and an element of the sample space
115    /// of a [`RandomVariableRange`].
116    ///
117    /// Note that **no guarantees** are made about whether or how the `rand`
118    /// parameter will be used. It may be sampled zero, one, or arbitrarily many
119    /// times. It may be used to sample values of type `R`, of type [`usize`],
120    /// or some other type. If some model of the random number generator is
121    /// available, then that model should be responsible for enumerating
122    /// possible outcomes.
123    fn fmap_rand_range<A: Inner, B: Inner, R: RandomVariable + SampleUniform, F: Fn(A, R) -> B>(
124        f: Self::Functor<A>,
125        range: impl RandomVariableRange<R>,
126        rng: &mut impl Rng,
127        func: F,
128    ) -> Self::Functor<B>
129    where
130        StandardUniform: Distribution<R>;
131}
132
133/// A [`RandomStrategy`] that supports an `fmap_flat` operation.
134///
135/// This requires a separate trait as, unlike `fmap`, a call to `fmap_flat` may
136/// require a functor to grow. This poses problems for stateless strategies.
137/// `PopulationSampler`, for instance, requires an [`Rng`] implementor to select
138/// which samples to discard.
139pub trait FlattenableRandomStrategy: RandomStrategy {
140    /// Applies the given function to the functor's inner, flattening one layer
141    /// of nested structure.
142    fn fmap_flat<A: Inner, B: Inner, F: FnMut(A) -> Self::Functor<B>>(
143        f: Self::Functor<A>,
144        func: F,
145    ) -> Self::Functor<B>;
146}
147
148/// A type that is enumerable and can be sampled from uniformly.
149///
150/// This trait requires that an implementor also implement
151/// [`Distribution<Self>`], to ensure that it can be sampled from. Additionally,
152/// a `sample_space` associated function must be provided.
153///
154/// Note that **a non-uniform distribution or a non-exhaustive sample space will
155/// result in a logic error**. In particular, this means that this trait should
156/// **not** be implemented for [`Option<T>`], as the probability of [`None`]
157/// being sampled is 0.5, regardless of the cardinality of the sample space of
158/// `T`.
159///
160/// # Provided Implementations
161///
162/// This crate provides implementations of `RandomVariable` for [`bool`] and all
163/// twelve built-in integer types.
164///
165/// Implementations are provided for [`u32`], [`u64`], [`u128`], [`usize`],
166/// [`i32`], [`i64`], [`i128`], and [`isize`] strictly for sampling from ranges
167/// (through [`RandomStrategy::fmap_rand_range`]). The use of
168/// [`RandomStrategy::fmap_rand`] with a 32-bit integer `RandomVariable` would
169/// involve, at minimum, a 4 GiB allocation just to enumerate the outcomes of a
170/// random process. This is obviously intractable on current computers.
171///
172/// # Implementing `RandomVariable`
173///
174/// Neither `Distribution<T> for Standard` nor `RandomVariable for T` are
175/// derivable. However, implementations for simple structs tends to follow a
176/// pattern. [`Distribution<Self>`] implementations will typically call
177/// `self.sample(rng)` for each field of the struct. `RandomVariable`
178/// implementations will typically use [`Iterator::flat_map`] to create a
179/// Cartesian product of all the sample spaces of the struct's fields.
180/// ```
181/// use rand::distr::StandardUniform;
182/// use rand::prelude::*;
183/// use rand_functors::RandomVariable;
184///
185/// #[derive(Clone, Debug, Eq, Hash, PartialEq)]
186/// struct Coordinate {
187///     x: u8,
188///     y: u8,
189/// }
190///
191/// impl Distribution<Coordinate> for StandardUniform {
192///     fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Coordinate {
193///         Coordinate {
194///             x: self.sample(rng),
195///             y: self.sample(rng),
196///         }
197///     }
198/// }
199///
200/// impl RandomVariable for Coordinate {
201///     fn sample_space() -> impl Iterator<Item = Self> {
202///         u8::sample_space().flat_map(|x| u8::sample_space().map(move |y| Coordinate { x, y }))
203///     }
204/// }
205/// ```
206pub trait RandomVariable: Sized
207where
208    StandardUniform: Distribution<Self>,
209{
210    /// Produce an [`Iterator`] containing all possible values of this type.
211    ///
212    /// This iterator must be finite, though a trait bound of
213    /// [`ExactSizeIterator`] is not specified, to allow the use of
214    /// [`Iterator::flat_map`] in implementations of this trait.
215    fn sample_space() -> impl Iterator<Item = Self>;
216}
217
218/// A (possibly inclusive) range of a [`RandomVariable`] that can be enumerated
219/// or sampled from.
220pub trait RandomVariableRange<R: RandomVariable + SampleUniform>
221where
222    StandardUniform: Distribution<R>,
223    Self: SampleRange<R>,
224{
225    /// Produce an [`Iterator`] containing all possible values in this range.
226    fn sample_space(&self) -> impl Iterator<Item = R>;
227}
228
229/// A container used by a [`RandomStrategy`] during computations.
230///
231/// In functional programming, the Functor pattern allows one to apply functions
232/// to values inside a container type, without changing the container's
233/// structure. A Functor must support an `fmap` operation, which applies the
234/// function passed to it as a parameter to the contents of the Functor. This
235/// operation is not a method required by Functor due to the limitations of
236/// Rust's type system.
237///
238/// Additionally, this trait requires that implementors provide the `pure`
239/// associated function. This provides for a way to begin a series of `fmap`,
240/// `fmap_flat`, and `fmap_rand` operations. This requirement technically puts
241/// this crate's functors halfway between "normal" functors and applicative
242/// functors, as a subset of the former and a superset of the latter. However,
243/// implementing full applicative functors would be unnecessary for the sorts of
244/// computations that this crate focuses on.
245pub trait Functor<I: Inner> {
246    /// Produce an instance of `Self` containing the argument as its inner.
247    ///
248    /// This associated function is often used to begin a series of
249    /// computations. The associated functions of [`RandomStrategy`] only
250    /// operate on the `Functor` associated with that [`RandomStrategy`].
251    fn pure(i: I) -> Self;
252}
253
254/// A valid inner type for a [`Functor`].
255///
256/// [`Clone`] is required because most non-trivial [`Functor`] implementations
257/// will need to clone their inner type. [`Eq`] and [`Hash`] are required to
258/// allow for [`Functor`] implementations involving maps and sets. It was
259/// determined that [`Hash`] was a less burdensome requirement than [`Ord`].
260pub trait Inner: Clone + Eq + Hash + PartialEq {}
261
262impl<T: Clone + Eq + Hash + PartialEq> Inner for T {}