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(missing_docs)]
61
62#[cfg(feature = "alloc")]
63extern crate alloc;
64
65pub use strategies::*;
66
67mod functors;
68mod random_variable_ranges;
69mod random_variables;
70mod strategies;
71
72use core::hash::Hash;
73
74use rand::distr::uniform::{SampleRange, SampleUniform};
75use rand::distr::StandardUniform;
76use rand::prelude::*;
77
78/// A strategy for evaluating sequences of functions of random data.
79///
80/// Types implementing `RandomStrategy` are typically not constructed. For this
81/// same reason, they are typically unit structs. Behaviour should be specified
82/// at compile-time, to allow calls to `fmap_rand` and `Functor::fmap` to be
83/// properly inlined.
84pub trait RandomStrategy {
85 /// The functor that this strategy operates on.
86 ///
87 /// Functions using a given strategy will typically return its associated
88 /// functor in the form `S::Functor<T>`.
89 type Functor<I: Inner>: Functor<I>;
90
91 /// Applies the given function to the functor's inner.
92 fn fmap<A: Inner, B: Inner, F: Fn(A) -> B>(f: Self::Functor<A>, func: F) -> Self::Functor<B>;
93
94 /// Using the strategy specified by the implementor, applies the given
95 /// binary function to the given functor and an element of the sample space
96 /// of a [`RandomVariable`].
97 ///
98 /// Note that **no guarantees** are made about whether or how the `rand`
99 /// parameter will be used. It may be sampled zero, one, or arbitrarily many
100 /// times. It may be used to sample values of type `R`, of type [`usize`],
101 /// or some other type. If some model of the random number generator is
102 /// available, then that model should be responsible for enumerating
103 /// possible outcomes.
104 fn fmap_rand<A: Inner, B: Inner, R: RandomVariable, F: Fn(A, R) -> B>(
105 f: Self::Functor<A>,
106 rng: &mut impl Rng,
107 func: F,
108 ) -> Self::Functor<B>
109 where
110 StandardUniform: Distribution<R>;
111
112 /// Using the strategy specified by the implementor, applies the given
113 /// binary function to the given functor and an element of the sample space
114 /// of a [`RandomVariableRange`].
115 ///
116 /// Note that **no guarantees** are made about whether or how the `rand`
117 /// parameter will be used. It may be sampled zero, one, or arbitrarily many
118 /// times. It may be used to sample values of type `R`, of type [`usize`],
119 /// or some other type. If some model of the random number generator is
120 /// available, then that model should be responsible for enumerating
121 /// possible outcomes.
122 fn fmap_rand_range<A: Inner, B: Inner, R: RandomVariable + SampleUniform, F: Fn(A, R) -> B>(
123 f: Self::Functor<A>,
124 range: impl RandomVariableRange<R>,
125 rng: &mut impl Rng,
126 func: F,
127 ) -> Self::Functor<B>
128 where
129 StandardUniform: Distribution<R>;
130}
131
132/// A [`RandomStrategy`] that supports an `fmap_flat` operation.
133///
134/// This requires a separate trait as, unlike `fmap`, a call to `fmap_flat` may
135/// require a functor to grow. This poses problems for stateless strategies.
136/// `PopulationSampler`, for instance, requires an [`Rng`] implementor to select
137/// which samples to discard.
138pub trait FlattenableRandomStrategy: RandomStrategy {
139 /// Applies the given function to the functor's inner, flattening one layer
140 /// of nested structure.
141 fn fmap_flat<A: Inner, B: Inner, F: FnMut(A) -> Self::Functor<B>>(
142 f: Self::Functor<A>,
143 func: F,
144 ) -> Self::Functor<B>;
145}
146
147/// A type that is enumerable and can be sampled from uniformly.
148///
149/// This trait requires that an implementor also implement
150/// [`Distribution<Self>`], to ensure that it can be sampled from. Additionally,
151/// a `sample_space` associated function must be provided.
152///
153/// Note that **a non-uniform distribution or a non-exhaustive sample space will
154/// result in a logic error**. In particular, this means that this trait should
155/// **not** be implemented for [`Option<T>`], as the probability of [`None`]
156/// being sampled is 0.5, regardless of the cardinality of the sample space of
157/// `T`.
158///
159/// # Provided Implementations
160///
161/// This crate provides implementations of `RandomVariable` for [`bool`] and all
162/// twelve built-in integer types.
163///
164/// Implementations are provided for [`u32`], [`u64`], [`u128`], [`usize`],
165/// [`i32`], [`i64`], [`i128`], and [`isize`] strictly for sampling from ranges
166/// (through [`RandomStrategy::fmap_rand_range`]). The use of
167/// [`RandomStrategy::fmap_rand`] with a 32-bit integer `RandomVariable` would
168/// involve, at minimum, a 4 GiB allocation just to enumerate the outcomes of a
169/// random process. This is obviously intractable on current computers.
170///
171/// # Implementing `RandomVariable`
172///
173/// Neither `Distribution<T> for Standard` nor `RandomVariable for T` are
174/// derivable. However, implementations for simple structs tends to follow a
175/// pattern. [`Distribution<Self>`] implementations will typically call
176/// `self.sample(rng)` for each field of the struct. `RandomVariable`
177/// implementations will typically use [`Iterator::flat_map`] to create a
178/// Cartesian product of all the sample spaces of the struct's fields.
179/// ```
180/// use rand::distr::StandardUniform;
181/// use rand::prelude::*;
182/// use rand_functors::RandomVariable;
183///
184/// #[derive(Clone, Debug, Eq, Hash, PartialEq)]
185/// struct Coordinate {
186/// x: u8,
187/// y: u8,
188/// }
189///
190/// impl Distribution<Coordinate> for StandardUniform {
191/// fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Coordinate {
192/// Coordinate {
193/// x: self.sample(rng),
194/// y: self.sample(rng),
195/// }
196/// }
197/// }
198///
199/// impl RandomVariable for Coordinate {
200/// fn sample_space() -> impl Iterator<Item = Self> {
201/// u8::sample_space().flat_map(|x| u8::sample_space().map(move |y| Coordinate { x, y }))
202/// }
203/// }
204/// ```
205pub trait RandomVariable: Sized
206where
207 StandardUniform: Distribution<Self>,
208{
209 /// Produce an [`Iterator`] containing all possible values of this type.
210 ///
211 /// This iterator must be finite, though a trait bound of
212 /// [`ExactSizeIterator`] is not specified, to allow the use of
213 /// [`Iterator::flat_map`] in implementations of this trait.
214 fn sample_space() -> impl Iterator<Item = Self>;
215}
216
217/// A (possibly inclusive) range of a [`RandomVariable`] that can be enumerated
218/// or sampled from.
219pub trait RandomVariableRange<R: RandomVariable + SampleUniform>
220where
221 StandardUniform: Distribution<R>,
222 Self: SampleRange<R>,
223{
224 /// Produce an [`Iterator`] containing all possible values in this range.
225 fn sample_space(&self) -> impl Iterator<Item = R>;
226}
227
228/// A container used by a [`RandomStrategy`] during computations.
229///
230/// In functional programming, the Functor pattern allows one to apply functions
231/// to values inside a container type, without changing the container's
232/// structure. A Functor must support an `fmap` operation, which applies the
233/// function passed to it as a parameter to the contents of the Functor. This
234/// operation is not a method required by Functor due to the limitations of
235/// Rust's type system.
236///
237/// Additionally, this trait requires that implementors provide the `pure`
238/// associated function. This provides for a way to begin a series of `fmap`,
239/// `fmap_flat`, and `fmap_rand` operations. This requirement technically puts
240/// this crate's functors halfway between "normal" functors and applicative
241/// functors, as a subset of the former and a superset of the latter. However,
242/// implementing full applicative functors would be unnecessary for the sorts of
243/// computations that this crate focuses on.
244pub trait Functor<I: Inner> {
245 /// Produce an instance of `Self` containing the argument as its inner.
246 ///
247 /// This associated function is often used to begin a series of
248 /// computations. The associated functions of [`RandomStrategy`] only
249 /// operate on the `Functor` associated with that [`RandomStrategy`].
250 fn pure(i: I) -> Self;
251}
252
253/// A valid inner type for a [`Functor`].
254///
255/// [`Clone`] is required because most non-trivial [`Functor`] implementations
256/// will need to clone their inner type. [`Eq`] and [`Hash`] are required to
257/// allow for [`Functor`] implementations involving maps and sets. It was
258/// determined that [`Hash`] was a less burdensome requirement than [`Ord`].
259pub trait Inner: Clone + Eq + Hash + PartialEq {}
260
261impl<T: Clone + Eq + Hash + PartialEq> Inner for T {}