fp_library/classes/monad_plus.rs
1//! Monads with a choice operator and identity element, combining [`Monad`](crate::classes::Monad) and [`Alternative`](crate::classes::Alternative).
2//!
3//! A `MonadPlus` is a type that supports both monadic sequencing
4//! ([`Monad`](crate::classes::Monad)) and choice with an identity element
5//! ([`Alternative`](crate::classes::Alternative)). It has no members of its own; it specifies that
6//! the type constructor has both `Monad` and `Alternative` instances.
7//!
8//! This module is a port of PureScript's
9//! [`Control.MonadPlus`](https://pursuit.purescript.org/packages/purescript-control/docs/Control.MonadPlus).
10//!
11//! # Hierarchy
12//!
13//! ```text
14//! Functor
15//! |
16//! +-- Pointed + Semiapplicative --> Applicative
17//! |
18//! +-- Applicative + Semimonad ----> Monad
19//! |
20//! +-- Alt + (identity element) ---> Plus
21//! |
22//! +-- Applicative + Plus ---------> Alternative
23//! |
24//! +-- Monad + Alternative --------> MonadPlus
25//! ```
26
27#[fp_macros::document_module(no_validation)]
28mod inner {
29 use crate::classes::*;
30
31 /// A type class for monads that also support choice, combining [`Monad`]
32 /// and [`Alternative`].
33 ///
34 /// `class (Monad m, Alternative m) => MonadPlus m`
35 ///
36 /// `MonadPlus` has no members of its own. It specifies that the type
37 /// constructor supports both monadic sequencing (`bind`, `pure`) and
38 /// choice with an identity element (`alt`, `empty`).
39 ///
40 /// # Laws
41 ///
42 /// A lawful `MonadPlus` must satisfy:
43 ///
44 /// - **Distributivity:** binding over a choice distributes:
45 ///
46 /// ```text
47 /// bind(alt(x, y), f) == alt(bind(x, f), bind(y, f))
48 /// ```
49 ///
50 /// The following property also holds for any `Monad + Alternative`:
51 ///
52 /// - **Left zero:** binding on the identity element yields the
53 /// identity element:
54 ///
55 /// ```text
56 /// bind(empty(), f) == empty()
57 /// ```
58 ///
59 /// # Examples
60 ///
61 /// Distributivity law for [`Vec`]:
62 ///
63 /// ```
64 /// use fp_library::{
65 /// brands::*,
66 /// functions::explicit::*,
67 /// };
68 ///
69 /// let x: Vec<i32> = vec![1, 2];
70 /// let y: Vec<i32> = vec![3, 4];
71 /// let f = |n: i32| vec![n * 10, n * 100];
72 ///
73 /// assert_eq!(
74 /// bind::<VecBrand, _, _, _, _>(alt::<VecBrand, _, _, _>(x.clone(), y.clone()), f),
75 /// alt::<VecBrand, _, _, _>(
76 /// bind::<VecBrand, _, _, _, _>(x, f),
77 /// bind::<VecBrand, _, _, _, _>(y, f)
78 /// ),
79 /// );
80 /// ```
81 ///
82 /// Left-zero law for [`Vec`]:
83 ///
84 /// ```
85 /// use fp_library::{
86 /// brands::*,
87 /// functions::{
88 /// explicit::bind,
89 /// *,
90 /// },
91 /// };
92 ///
93 /// let f = |n: i32| vec![n * 2];
94 ///
95 /// assert_eq!(
96 /// bind::<VecBrand, _, _, _, _>(plus_empty::<VecBrand, i32>(), f),
97 /// plus_empty::<VecBrand, i32>(),
98 /// );
99 /// ```
100 ///
101 /// # Implementors
102 ///
103 /// The blanket implementation applies to every brand with both [`Monad`]
104 /// and [`Alternative`]. The following brands are known to be *lawful*
105 /// (satisfying distributivity and left zero):
106 ///
107 /// - [`VecBrand`](crate::brands::VecBrand)
108 /// - [`CatListBrand`](crate::brands::CatListBrand)
109 ///
110 /// Note that [`OptionBrand`](crate::brands::OptionBrand) acquires the
111 /// trait via the blanket impl but does *not* satisfy the distributivity
112 /// law, because `alt` for `Option` picks the first `Some` and discards
113 /// the second branch entirely.
114 pub trait MonadPlus: Monad + Alternative {}
115
116 /// Blanket implementation of [`MonadPlus`].
117 #[document_type_parameters("The brand type.")]
118 impl<Brand> MonadPlus for Brand where Brand: Monad + Alternative {}
119}
120
121pub use inner::*;
122
123#[cfg(test)]
124mod tests {
125 use {
126 crate::{
127 brands::*,
128 functions::{
129 explicit::{
130 alt,
131 bind,
132 },
133 *,
134 },
135 types::cat_list::CatList,
136 },
137 quickcheck_macros::quickcheck,
138 };
139
140 // -- Distributivity: bind(alt(x, y), f) == alt(bind(x, f), bind(y, f)) --
141
142 /// Tests the distributivity law for MonadPlus with VecBrand.
143 #[quickcheck]
144 fn distributivity_vec(
145 x: Vec<i32>,
146 y: Vec<i32>,
147 ) -> bool {
148 let f = |n: i32| {
149 if n > 0 { vec![n.wrapping_mul(2)] } else { vec![] }
150 };
151 bind::<VecBrand, _, _, _, _>(alt::<VecBrand, _, _, _>(x.clone(), y.clone()), f)
152 == alt::<VecBrand, _, _, _>(
153 bind::<VecBrand, _, _, _, _>(x, f),
154 bind::<VecBrand, _, _, _, _>(y, f),
155 )
156 }
157
158 /// Tests the distributivity law for MonadPlus with CatListBrand.
159 #[quickcheck]
160 fn distributivity_cat_list(
161 xv: Vec<i32>,
162 yv: Vec<i32>,
163 ) -> bool {
164 let x: CatList<i32> = xv.into_iter().collect();
165 let y: CatList<i32> = yv.into_iter().collect();
166 let f = |n: i32| -> CatList<i32> {
167 if n > 0 { CatList::singleton(n.wrapping_mul(2)) } else { CatList::empty() }
168 };
169 bind::<CatListBrand, _, _, _, _>(alt::<CatListBrand, _, _, _>(x.clone(), y.clone()), f)
170 == alt::<CatListBrand, _, _, _>(
171 bind::<CatListBrand, _, _, _, _>(x, f),
172 bind::<CatListBrand, _, _, _, _>(y, f),
173 )
174 }
175
176 // -- Left zero: bind(empty(), f) == empty() --
177
178 /// Tests the left-zero law for MonadPlus with OptionBrand.
179 #[test]
180 fn left_zero_option() {
181 let f = |n: i32| if n > 0 { Some(n * 2) } else { None };
182 assert_eq!(
183 bind::<OptionBrand, _, _, _, _>(plus_empty::<OptionBrand, i32>(), f),
184 plus_empty::<OptionBrand, i32>(),
185 );
186 }
187
188 /// Tests the left-zero law for MonadPlus with VecBrand.
189 #[test]
190 fn left_zero_vec() {
191 let f = |n: i32| if n > 0 { vec![n * 2] } else { vec![] };
192 assert_eq!(
193 bind::<VecBrand, _, _, _, _>(plus_empty::<VecBrand, i32>(), f),
194 plus_empty::<VecBrand, i32>(),
195 );
196 }
197
198 /// Tests the left-zero law for MonadPlus with CatListBrand.
199 #[test]
200 fn left_zero_cat_list() {
201 let f = |n: i32| -> CatList<i32> {
202 if n > 0 { CatList::singleton(n * 2) } else { CatList::empty() }
203 };
204 assert_eq!(
205 bind::<CatListBrand, _, _, _, _>(plus_empty::<CatListBrand, i32>(), f),
206 plus_empty::<CatListBrand, i32>(),
207 );
208 }
209}