Skip to main content

cliffy_core/
combinators.rs

1//! Combinators for composing behaviors
2//!
3//! These combinators provide algebraic ways to compose reactive values,
4//! inspired by classical FRP semantics. They replace imperative control
5//! flow with declarative compositions.
6
7use crate::behavior::{behavior, Behavior};
8use crate::geometric::{FromGeometric, IntoGeometric};
9
10/// Conditional combinator - select between values based on a condition
11///
12/// Creates a behavior that follows the value of `then_value` when `condition`
13/// is true, otherwise it holds `None`.
14///
15/// # Example
16///
17/// ```rust
18/// use cliffy_core::{behavior, when};
19///
20/// let show_message = behavior(true);
21/// let message = when(&show_message, || "Hello!".to_string());
22///
23/// assert_eq!(message.sample(), Some("Hello!".to_string()));
24///
25/// show_message.set(false);
26/// assert_eq!(message.sample(), None);
27/// ```
28pub fn when<T, F>(condition: &Behavior<bool>, then_value: F) -> Behavior<Option<T>>
29where
30    T: IntoGeometric + FromGeometric + Clone + Default + 'static,
31    F: Fn() -> T + 'static,
32{
33    let initial = if condition.sample() {
34        Some(then_value())
35    } else {
36        None
37    };
38
39    let result = behavior(initial);
40    let result_clone = result.clone();
41
42    condition.subscribe(move |&cond| {
43        if cond {
44            result_clone.set(Some(then_value()));
45        } else {
46            result_clone.set(None);
47        }
48    });
49
50    result
51}
52
53/// Combine two behaviors into one
54///
55/// Creates a behavior whose value is computed from both input behaviors
56/// using the provided function.
57///
58/// # Example
59///
60/// ```rust
61/// use cliffy_core::{behavior, combine};
62///
63/// let a = behavior(10i32);
64/// let b = behavior(20i32);
65/// let sum = combine(&a, &b, |x, y| x + y);
66///
67/// assert_eq!(sum.sample(), 30);
68/// ```
69pub fn combine<A, B, C, F>(a: &Behavior<A>, b: &Behavior<B>, f: F) -> Behavior<C>
70where
71    A: IntoGeometric + FromGeometric + Clone + 'static,
72    B: IntoGeometric + FromGeometric + Clone + 'static,
73    C: IntoGeometric + FromGeometric + Clone + 'static,
74    F: Fn(A, B) -> C + Clone + 'static,
75{
76    a.combine(b, f)
77}
78
79/// Combine three behaviors into one
80///
81/// # Example
82///
83/// ```rust
84/// use cliffy_core::{behavior, combinators::combine3};
85///
86/// let a = behavior(1i32);
87/// let b = behavior(2i32);
88/// let c = behavior(3i32);
89/// let sum = combine3(&a, &b, &c, |x, y, z| x + y + z);
90///
91/// assert_eq!(sum.sample(), 6);
92/// ```
93pub fn combine3<A, B, C, D, F>(
94    a: &Behavior<A>,
95    b: &Behavior<B>,
96    c: &Behavior<C>,
97    f: F,
98) -> Behavior<D>
99where
100    A: IntoGeometric + FromGeometric + Clone + 'static,
101    B: IntoGeometric + FromGeometric + Clone + 'static,
102    C: IntoGeometric + FromGeometric + Clone + 'static,
103    D: IntoGeometric + FromGeometric + Clone + 'static,
104    F: Fn(A, B, C) -> D + Clone + 'static,
105{
106    let ab = a.combine(b, |x, y| (x, y));
107    ab.combine(c, move |(x, y), z| f(x, y, z))
108}
109
110/// If-then-else combinator
111///
112/// Creates a behavior that follows `then_value` when `condition` is true,
113/// otherwise follows `else_value`.
114///
115/// # Example
116///
117/// ```rust
118/// use cliffy_core::{behavior, combinators::if_else};
119///
120/// let is_admin = behavior(false);
121/// let greeting = if_else(
122///     &is_admin,
123///     || "Welcome, Admin!".to_string(),
124///     || "Welcome, User!".to_string(),
125/// );
126///
127/// assert_eq!(greeting.sample(), "Welcome, User!");
128///
129/// is_admin.set(true);
130/// assert_eq!(greeting.sample(), "Welcome, Admin!");
131/// ```
132pub fn if_else<T, TF, EF>(condition: &Behavior<bool>, then_value: TF, else_value: EF) -> Behavior<T>
133where
134    T: IntoGeometric + FromGeometric + Clone + 'static,
135    TF: Fn() -> T + 'static,
136    EF: Fn() -> T + 'static,
137{
138    let initial = if condition.sample() {
139        then_value()
140    } else {
141        else_value()
142    };
143
144    let result = behavior(initial);
145    let result_clone = result.clone();
146
147    condition.subscribe(move |&cond| {
148        if cond {
149            result_clone.set(then_value());
150        } else {
151            result_clone.set(else_value());
152        }
153    });
154
155    result
156}
157
158/// Constant behavior that never changes
159///
160/// # Example
161///
162/// ```rust
163/// use cliffy_core::combinators::constant;
164///
165/// let pi = constant(3.14159);
166/// assert_eq!(pi.sample(), 3.14159);
167/// ```
168pub fn constant<T>(value: T) -> Behavior<T>
169where
170    T: IntoGeometric + FromGeometric + Clone + 'static,
171{
172    behavior(value)
173}
174
175/// Map multiple behaviors with a function
176///
177/// This is useful when you need to transform multiple behaviors at once.
178pub fn map2<A, B, C, F>(a: &Behavior<A>, b: &Behavior<B>, f: F) -> Behavior<C>
179where
180    A: IntoGeometric + FromGeometric + Clone + 'static,
181    B: IntoGeometric + FromGeometric + Clone + 'static,
182    C: IntoGeometric + FromGeometric + Clone + 'static,
183    F: Fn(A, B) -> C + Clone + 'static,
184{
185    combine(a, b, f)
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_when_true() {
194        let condition = behavior(true);
195        let result = when(&condition, || 42i32);
196        assert_eq!(result.sample(), Some(42));
197    }
198
199    #[test]
200    fn test_when_false() {
201        let condition = behavior(false);
202        let result = when(&condition, || 42i32);
203        assert_eq!(result.sample(), None);
204    }
205
206    #[test]
207    fn test_when_reactive() {
208        let condition = behavior(true);
209        let result = when(&condition, || "visible".to_string());
210
211        assert_eq!(result.sample(), Some("visible".to_string()));
212
213        condition.set(false);
214        assert_eq!(result.sample(), None);
215
216        condition.set(true);
217        assert_eq!(result.sample(), Some("visible".to_string()));
218    }
219
220    #[test]
221    fn test_combine() {
222        let a = behavior(10i32);
223        let b = behavior(5i32);
224        let sum = combine(&a, &b, |x, y| x + y);
225
226        assert_eq!(sum.sample(), 15);
227
228        a.set(20);
229        assert_eq!(sum.sample(), 25);
230    }
231
232    #[test]
233    fn test_combine3() {
234        let a = behavior(1i32);
235        let b = behavior(2i32);
236        let c = behavior(3i32);
237        let sum = combine3(&a, &b, &c, |x, y, z| x + y + z);
238
239        assert_eq!(sum.sample(), 6);
240    }
241
242    #[test]
243    fn test_if_else() {
244        let is_dark_mode = behavior(false);
245        let theme = if_else(&is_dark_mode, || "dark".to_string(), || "light".to_string());
246
247        assert_eq!(theme.sample(), "light");
248
249        is_dark_mode.set(true);
250        assert_eq!(theme.sample(), "dark");
251    }
252
253    #[test]
254    fn test_constant() {
255        let c = constant(42i32);
256        assert_eq!(c.sample(), 42);
257    }
258}