Skip to main content

nami/reactive_core/
zip.rs

1//! Provides functionality for combining and transforming computations.
2//!
3//! This module contains:
4//! - `Zip`: A structure to combine two `Signal` instances into one computation
5//!   that produces a tuple of their results.
6//! - `FlattenMap`: A trait for flattening and mapping nested tuple structures,
7//!   which simplifies working with multiple zipped computations.
8//!
9//! These utilities enable composition of reactive computations, making it easier
10//! to work with multiple interdependent values in a reactive context.
11
12use alloc::rc::Rc;
13use core::{cell::RefCell, panic::Location};
14
15use crate::{Signal, SignalIdentity, map::Map, watcher::Context};
16
17/// A structure that combines two `Signal` instances into a single computation
18/// that produces a tuple of their results.
19#[derive(Debug, Clone)]
20pub struct Zip<A, B> {
21    /// The first computation to be zipped.
22    a: A,
23    /// The second computation to be zipped.
24    b: B,
25    discriminator: usize,
26}
27
28struct ZipWatchState<L, R, W> {
29    latest_left: RefCell<L>,
30    latest_right: RefCell<R>,
31    watcher: W,
32}
33
34impl<A, B> Zip<A, B>
35where
36    A: Signal,
37    B: Signal,
38    A::Output: Clone,
39    B::Output: Clone,
40{
41    /// Creates a new `Zip` instance by combining two computations.
42    ///
43    /// # Parameters
44    /// - `a`: The first computation to be zipped.
45    /// - `b`: The second computation to be zipped.
46    ///
47    /// # Returns
48    /// A new `Zip` instance containing both computations.
49    /// Creates a new `Zip` that combines two signals.
50    #[track_caller]
51    pub fn new(a: A, b: B) -> Self {
52        Self {
53            a,
54            b,
55            discriminator: SignalIdentity::call_site_discriminator::<(A, B)>(Location::caller()),
56        }
57    }
58}
59
60/// This trait provides a way to apply a function to the individual elements
61/// of a nested tuple structure, flattening the structure in the process.
62pub trait FlattenMap<F, T, Output>: Signal {
63    /// Maps a function over the flattened elements of a nested tuple.
64    ///
65    /// # Parameters
66    /// - `self`: The computation that produces a nested tuple.
67    /// - `f`: The function to apply to the flattened elements.
68    ///
69    /// # Returns
70    /// A new computation that produces the result of applying `f` to the flattened elements.
71    #[track_caller]
72    fn flatten_map(&self, f: F) -> Map<Self, impl Clone + Fn(Self::Output) -> Output, Output>;
73}
74
75/// Implementation for flattening and mapping a tuple of two elements.
76impl<C, F, T1, T2, Output> FlattenMap<F, (T1, T2), Output> for C
77where
78    C: Signal<Output = (T1, T2)> + 'static,
79    F: 'static + Clone + Fn(T1, T2) -> Output,
80    T1: 'static,
81    T2: 'static,
82    Output: 'static,
83{
84    #[track_caller]
85    fn flatten_map(&self, f: F) -> Map<C, impl Clone + Fn((T1, T2)) -> Output, Output> {
86        Map::new(self.clone(), move |(t1, t2)| f(t1, t2))
87    }
88}
89
90/// Implementation for flattening and mapping a tuple of three elements.
91impl<C, F, T1, T2, T3, Output> FlattenMap<F, (T1, T2, T3), Output> for C
92where
93    C: Signal<Output = ((T1, T2), T3)> + 'static,
94    F: 'static + Clone + Fn(T1, T2, T3) -> Output,
95    Output: 'static,
96{
97    #[track_caller]
98    fn flatten_map(&self, f: F) -> Map<C, impl Clone + Fn(((T1, T2), T3)) -> Output, Output> {
99        Map::new(self.clone(), move |((t1, t2), t3)| f(t1, t2, t3))
100    }
101}
102
103/// Creates a new `Zip` computation that combines two separate computations.
104///
105/// This function is a convenience wrapper around `Zip::new`.
106///
107/// # Parameters
108/// - `a`: The first computation to zip.
109/// - `b`: The second computation to zip.
110///
111/// # Returns
112/// A new `Zip` instance that computes both values and returns them as a tuple.
113#[track_caller]
114pub fn zip<A, B>(a: A, b: B) -> Zip<A, B>
115where
116    A: Signal,
117    B: Signal,
118    A::Output: Clone,
119    B::Output: Clone,
120{
121    Zip::new(a, b)
122}
123
124/// Implementation of the `Signal` trait for `Zip`.
125impl<A, B> Signal for Zip<A, B>
126where
127    A: Signal,
128    B: Signal,
129    A::Output: Clone,
130    B::Output: Clone,
131{
132    /// The output type of the zipped computation is a tuple of the outputs of the individual computations.
133    type Output = (A::Output, B::Output);
134    type Guard = (A::Guard, B::Guard);
135
136    /// Computes both values and returns them as a tuple.
137    ///
138    /// # Returns
139    /// A tuple containing the results of computing `a` and `b`.
140    fn get(&self) -> Self::Output {
141        let Self { a, b, .. } = self;
142        (a.get(), b.get())
143    }
144
145    fn identity(&self) -> Option<SignalIdentity> {
146        Some(
147            self.a
148                .identity()?
149                .combine(self.b.identity()?)
150                .with_discriminator(self.discriminator),
151        )
152    }
153
154    /// Adds a watcher to the zipped computation.
155    ///
156    /// This method sets up watchers for both `a` and `b` such that when either one
157    /// changes, the watcher for the `Zip` is notified with the new tuple.
158    ///
159    /// # Parameters
160    /// - `watcher`: The watcher to notify when either computation changes.
161    ///
162    /// # Returns
163    /// A `WatcherGuard` that, when dropped, will remove the watchers from both computations.
164    fn watch(&self, watcher: impl Fn(Context<Self::Output>) + 'static) -> Self::Guard {
165        let Self { a, b, .. } = self;
166        let state = Rc::new(ZipWatchState {
167            latest_left: RefCell::new(a.get()),
168            latest_right: RefCell::new(b.get()),
169            watcher,
170        });
171
172        let guard_a = {
173            let state = Rc::clone(&state);
174            self.a.watch(move |ctx: Context<A::Output>| {
175                let updated_a = ctx.value().clone();
176                *state.latest_left.borrow_mut() = updated_a;
177                let other = state.latest_right.borrow().clone();
178                (state.watcher)(ctx.map(|value| (value, other)));
179            })
180        };
181
182        let guard_b = self.b.watch(move |ctx: Context<B::Output>| {
183            let updated_b = ctx.value().clone();
184            *state.latest_right.borrow_mut() = updated_b;
185            let other = state.latest_left.borrow().clone();
186            (state.watcher)(ctx.map(|value| (other, value)));
187        });
188
189        (guard_a, guard_b)
190    }
191}