open_hypergraphs/indexed_coproduct/
arrow.rs

1use crate::array::*;
2use crate::category::*;
3use crate::finite_function::*;
4use crate::semifinite::*;
5
6use core::fmt::Debug;
7use core::ops::{Add, Shr};
8use num_traits::{One, Zero};
9
10// The minimum set of operations some arrows must have in order to define an [`IndexedCoproduct`]
11// over them.
12pub trait HasLen<K: ArrayKind> {
13    fn len(&self) -> K::I;
14    fn is_empty(&self) -> bool {
15        self.len() == K::I::zero()
16    }
17}
18
19impl<K: ArrayKind> HasLen<K> for FiniteFunction<K> {
20    fn len(&self) -> K::I {
21        self.source()
22    }
23}
24
25impl<K: ArrayKind, T> HasLen<K> for SemifiniteFunction<K, T>
26where
27    K::Type<T>: Array<K, T>,
28{
29    fn len(&self) -> K::I {
30        self.0.len()
31    }
32}
33
34// TODO: replace sources with a FiniteFunction<K> of *pointers* whose codomain is total size?
35// This lets us remove a lot of trait bounds.
36/// A finite coproduct of arrows of type `A`.
37/// Pragmatically, it's a segmented array
38#[non_exhaustive] // force construction via new.
39pub struct IndexedCoproduct<K: ArrayKind, F> {
40    /// A ['FiniteFunction'] whose sum is the length of `values`, and whose target is `sum + 1`.
41    pub sources: FiniteFunction<K>,
42
43    /// The concatenation of all arrays in the coproduct.
44    pub values: F,
45}
46
47impl<K: ArrayKind, F: Clone> Clone for IndexedCoproduct<K, F>
48where
49    K::Type<K::I>: Clone,
50{
51    fn clone(&self) -> Self {
52        Self {
53            sources: self.sources.clone(),
54            values: self.values.clone(),
55        }
56    }
57}
58
59impl<K: ArrayKind, F: PartialEq> PartialEq for IndexedCoproduct<K, F> {
60    fn eq(&self, other: &Self) -> bool {
61        self.sources == other.sources && self.values == other.values
62    }
63}
64
65impl<K: ArrayKind, F: Clone + HasLen<K>> IndexedCoproduct<K, F>
66where
67    K::Type<K::I>: NaturalArray<K>,
68{
69    /// Create a new IndexedCoproduct from a FiniteFunction whose target is the sum of its
70    /// elements. This condition is checked by summing the array.
71    pub fn new(sources: FiniteFunction<K>, values: F) -> Option<Self> {
72        IndexedCoproduct { sources, values }.validate()
73    }
74
75    /// Create a [`IndexedCoproduct`] from an *array* of sources by computing their sum to create a
76    /// [`FiniteFunction`].
77    pub fn from_semifinite(sources: SemifiniteFunction<K, K::I>, values: F) -> Option<Self> {
78        let sources = FiniteFunction::new(sources.0.into(), values.len() + K::I::one())?;
79        IndexedCoproduct { sources, values }.validate()
80    }
81
82    fn validate(self) -> Option<Self> {
83        let sum = self.sources.table.sum();
84
85        // Target of 'sources' must equal its sum + 1
86        if self.sources.target != sum.clone() + K::I::one() {
87            return None;
88        }
89
90        if sum != self.values.len() {
91            return None;
92        }
93
94        Some(self)
95    }
96}
97
98impl<K: ArrayKind, F: Clone + HasLen<K>> IndexedCoproduct<K, F>
99where
100    K::Type<K::I>: NaturalArray<K>,
101{
102    /// Construct a segmented array with a single segment containing values.
103    pub fn singleton(values: F) -> Self {
104        let n = values.len();
105        let sources = FiniteFunction::constant(K::I::one(), n, K::I::zero());
106        IndexedCoproduct { sources, values }
107    }
108
109    /// Construct a segmented array with `values.len()` segments, each containing a single element.
110    pub fn elements(values: F) -> Self {
111        let n = values.len();
112
113        // Construct the sources array directly: an array of constant 1s with target n+1.
114        let sources =
115            FiniteFunction::new(K::Index::fill(K::I::one(), n.clone()), n + K::I::one()).unwrap();
116
117        //let sources = FiniteFunction::terminal(n.clone()).inject1(n);
118        IndexedCoproduct::new(sources, values).expect("by construction")
119    }
120
121    pub fn len(&self) -> K::I {
122        self.sources.source()
123    }
124
125    /// Like [`IndexedCoproduct::flatmap`], but where the values of `other` are already mapped.
126    ///
127    /// Conceptually, suppose we have
128    ///
129    /// ```text
130    /// self : [[T]]
131    /// other: [[U]]
132    /// ```
133    ///
134    /// where `other` defines a sublist for each element of `join(self)`.
135    /// Then `self.flatmap_sources(other)` merges sublists of `other` using the sources of `self`.
136    pub fn flatmap_sources<G: Clone>(
137        &self,
138        other: &IndexedCoproduct<K, G>,
139    ) -> IndexedCoproduct<K, G> {
140        // Total length of all sublists in self must equal *number* of sublists in other.
141        // That is, For each value in concatenated self, we have a sublist in other.
142        assert_eq!(self.values.len(), other.len());
143
144        let sources = FiniteFunction {
145            table: self.sources.table.segmented_sum(&other.sources.table),
146            target: other.sources.target.clone(), // TODO: write a test for this
147        };
148        let values = other.values.clone();
149        IndexedCoproduct { sources, values }
150    }
151}
152
153impl<K: ArrayKind, F> HasLen<K> for IndexedCoproduct<K, F>
154where
155    K::Type<K::I>: NaturalArray<K>,
156{
157    fn len(&self) -> K::I {
158        self.sources.len()
159    }
160}
161
162// Special case methods where the values are finite functions.
163impl<K: ArrayKind> IndexedCoproduct<K, FiniteFunction<K>>
164where
165    K::Type<K::I>: NaturalArray<K>,
166{
167    /// The initial object, i.e., the finite coproduct indexed by the empty set
168    /// Note that the target of `sources` must be zero for laws to work here.
169    pub fn initial(target: K::I) -> Self {
170        let sources = FiniteFunction::initial(K::I::one());
171        let values = FiniteFunction::initial(target);
172        IndexedCoproduct { sources, values }
173    }
174
175    // This could generalise to any type with a tensor product, but we only need it for finite functions
176    pub fn tensor(
177        &self,
178        other: &IndexedCoproduct<K, FiniteFunction<K>>,
179    ) -> IndexedCoproduct<K, FiniteFunction<K>> {
180        // build a new finite function for 'sources'. it consists of:
181        //  - concatenated segment sizes
182        //  - target equal to *total sum* (sum of targets)
183        let table = self.sources.table.concatenate(&other.sources.table);
184        let target = (self.sources.target.clone() + other.sources.target.clone()) - K::I::one();
185
186        IndexedCoproduct {
187            sources: FiniteFunction { table, target },
188            values: &self.values | &other.values,
189        }
190    }
191
192    /// Map the *values* array of an indexed coproduct, leaving the sources unchanged.
193    ///
194    /// Given an indexed coproduct
195    ///
196    /// ```text
197    /// Σ_{i ∈ I} f_i : Σ_{i ∈ I} A_i → B
198    /// ```
199    ///
200    /// and a finite function `x : B → C`,
201    /// return a new [`IndexedCoproduct`] representing
202    ///
203    /// ```text
204    /// Σ_{i ∈ I} (f_i ; x) : Σ_{i ∈ I} A_i → C
205    /// ```
206    ///
207    /// Returns `None` if `x.source() != B`.
208    pub fn map_values(&self, x: &FiniteFunction<K>) -> Option<Self> {
209        Some(Self {
210            sources: self.sources.clone(),
211            values: (&self.values >> x)?,
212        })
213    }
214
215    // TODO: FIXME: including this is annoying. Can we roll map_values and map_semifinite into one
216    // function by just requiring the `F` parameter to be post-composable with FiniteFunction?
217    /// Same as `map_values`, but for `SemifiniteFunction`.
218    pub fn map_semifinite<T>(
219        &self,
220        x: &SemifiniteFunction<K, T>,
221    ) -> Option<IndexedCoproduct<K, SemifiniteFunction<K, T>>>
222    where
223        K::Type<T>: Array<K, T>,
224    {
225        Some(IndexedCoproduct::<K, SemifiniteFunction<K, T>> {
226            sources: self.sources.clone(),
227            values: (&self.values >> x)?,
228        })
229    }
230
231    /// Compose two [`IndexedCoproduct`] thought of as lists-of-lists.
232    /// Given
233    ///
234    /// ```text
235    /// self : Σ_{a ∈ A} s(a) → B      aka A → B*
236    /// other : Σ_{b ∈ B} s(b) → C      aka B → C*
237    /// ```
238    ///
239    /// we obtain
240    ///
241    /// ```text
242    /// self.flatmap(other) : Σ_{a ∈ A} s(a) → C     aka A → C*
243    /// ```
244    pub fn flatmap(&self, other: &Self) -> Self {
245        assert_eq!(self.values.target(), other.len());
246
247        let sources_table = self
248            .sources
249            .table
250            .segmented_sum(&(&self.values >> &other.sources).unwrap().table);
251
252        let values = &other.sources.injections(&self.values).unwrap() >> &other.values;
253        let values = values.unwrap();
254
255        IndexedCoproduct::from_semifinite(SemifiniteFunction(sources_table.into()), values).unwrap()
256    }
257}
258
259// Special case methods for SemifiniteFunction
260// NOTE: this is a bit of a hack: it requires Shr and Add instances for the values type (F)
261// so this impl works for both SemifiniteFunction and FiniteFunction values arrays.
262impl<K: ArrayKind, F> IndexedCoproduct<K, F>
263where
264    K::Type<K::I>: NaturalArray<K>,
265    F: HasLen<K> + Clone,
266    for<'a, 'b> &'a FiniteFunction<K>: Shr<&'b F, Output = Option<F>>, // compose
267    for<'a, 'b> &'a F: Add<&'b F, Output = Option<F>>,                 // coproduct
268{
269    pub fn coproduct(&self, other: &Self) -> Option<Self> {
270        // build a new finite function for 'sources'. it consists of:
271        //  - concatenated segment sizes
272        //  - target equal to *total sum* (sum of targets)
273        let table = self.sources.table.concatenate(&other.sources.table);
274        let target = (self.sources.target.clone() + other.sources.target.clone()) - K::I::one();
275
276        // NOTE: this might fail if the two underlying FiniteFunctions do not share a codomain.
277        Some(IndexedCoproduct {
278            sources: FiniteFunction { table, target },
279            values: (&self.values + &other.values)?,
280        })
281    }
282
283    /// Given an [`IndexedCoproduct`] of [`SemifiniteFunction`]:
284    ///
285    /// ```text
286    /// Σ_{i ∈ X} f_i : Σ_{i ∈ X} A_i → B
287    /// ```
288    ///
289    /// and a finite function `x : W → X`
290    ///
291    /// Return a new [`IndexedCoproduct`] representing
292    ///
293    /// ```text
294    /// Σ_{i ∈ W} f_{x(i)} : Σ_{i ∈ W} A_{x(i)} → B
295    /// ```
296    pub fn map_indexes(&self, x: &FiniteFunction<K>) -> Option<Self> {
297        let sources = x.compose(&self.sources)?;
298        let values = self.indexed_values(x)?;
299        IndexedCoproduct::from_semifinite(SemifiniteFunction(sources.table.into()), values)
300    }
301
302    /// Like [`Self::map_indexes`] but only returns the values array.
303    pub fn indexed_values(&self, x: &FiniteFunction<K>) -> Option<F> {
304        &self.sources.injections(x)? >> &self.values
305    }
306}
307
308impl<K: ArrayKind, F: Debug> Debug for IndexedCoproduct<K, F>
309where
310    K::Index: Debug,
311{
312    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313        f.debug_struct("IndexedCoproduct")
314            .field("sources", &self.sources)
315            .field("values", &self.values)
316            .finish()
317    }
318}