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}