Skip to main content

vortex_utils/
iter.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Iterator extension traits.
5
6use std::convert::Infallible;
7
8/// An extension trait for iterators that provides balanced binary tree reduction.
9///
10/// Unlike [`Iterator::reduce`], which builds a left-leaning linear chain of depth N,
11/// `reduce_balanced` builds a balanced binary tree of depth log(N). This avoids deep
12/// nesting that can cause stack overflows on drop or suboptimal evaluation.
13///
14/// ```text
15/// reduce:          reduce_balanced:
16///     f                  f
17///    / \                / \
18///   f   d              f   f
19///  / \                / \ / \
20/// f   c              a  b c  d
21/// |\
22/// a b
23/// ```
24pub trait ReduceBalancedIterExt: Iterator {
25    /// Like [`Iterator::reduce`], but builds a balanced binary tree instead of a linear chain.
26    ///
27    /// `[a, b, c, d]` becomes `combine(combine(a, b), combine(c, d))`.
28    ///
29    /// Returns `None` if the iterator is empty.
30    fn reduce_balanced<F>(self, combine: F) -> Option<Self::Item>
31    where
32        F: Fn(Self::Item, Self::Item) -> Self::Item;
33
34    /// Fallible version of [`reduce_balanced`](ReduceBalancedIterExt::reduce_balanced).
35    ///
36    /// Short-circuits on the first error.
37    fn try_reduce_balanced<F, E>(self, combine: F) -> Result<Option<Self::Item>, E>
38    where
39        F: Fn(Self::Item, Self::Item) -> Result<Self::Item, E>;
40}
41
42impl<I: Iterator + Sized> ReduceBalancedIterExt for I {
43    fn reduce_balanced<F>(self, combine: F) -> Option<Self::Item>
44    where
45        F: Fn(Self::Item, Self::Item) -> Self::Item,
46    {
47        match self.try_reduce_balanced(|lhs, rhs| Ok::<_, Infallible>(combine(lhs, rhs))) {
48            Ok(result) => result,
49            Err(never) => match never {},
50        }
51    }
52
53    fn try_reduce_balanced<F, E>(self, combine: F) -> Result<Option<Self::Item>, E>
54    where
55        F: Fn(Self::Item, Self::Item) -> Result<Self::Item, E>,
56    {
57        let mut items: Vec<_> = self.collect();
58        if items.is_empty() {
59            return Ok(None);
60        }
61        if items.len() == 1 {
62            return Ok(items.pop());
63        }
64
65        // Each pass consumes one level of the reduction tree, combining adjacent pairs into the
66        // next level. The two vectors swap roles between passes so items can be moved rather than
67        // cloned while retaining their allocations.
68        let mut next = Vec::with_capacity(items.len() / 2);
69        while items.len() > 1 {
70            next.clear();
71            let mut iter = items.drain(..);
72            while let Some(lhs) = iter.next() {
73                if let Some(rhs) = iter.next() {
74                    next.push(combine(lhs, rhs)?);
75                } else {
76                    // Folding an odd tail into the preceding pair keeps it at the current tree
77                    // level instead of carrying it forward as a shallower subtree.
78                    let Some(previous) = next.pop() else {
79                        unreachable!("a reduction level with an odd tail has a preceding pair")
80                    };
81                    next.push(combine(previous, lhs)?);
82                }
83            }
84            drop(iter);
85            std::mem::swap(&mut items, &mut next);
86        }
87
88        assert_eq!(items.len(), 1);
89        Ok(items.pop())
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_empty() {
99        let result = std::iter::empty::<i32>().reduce_balanced(|a, b| a + b);
100        assert_eq!(result, None);
101    }
102
103    #[test]
104    fn test_single() {
105        let result = [42].into_iter().reduce_balanced(|a, b| a + b);
106        assert_eq!(result, Some(42));
107    }
108
109    #[test]
110    fn test_two() {
111        let result = [1, 2].into_iter().reduce_balanced(|a, b| a + b);
112        assert_eq!(result, Some(3));
113    }
114
115    #[test]
116    fn test_power_of_two() {
117        let result = [1, 2, 3, 4].into_iter().reduce_balanced(|a, b| a + b);
118        assert_eq!(result, Some(10));
119    }
120
121    #[test]
122    fn test_odd_count() {
123        let result = [1, 2, 3, 4, 5].into_iter().reduce_balanced(|a, b| a + b);
124        assert_eq!(result, Some(15));
125    }
126
127    #[test]
128    fn test_balanced_structure() {
129        // Use string concatenation to verify the tree shape.
130        // [a, b, c, d] should produce ((a+b)+(c+d)), not (((a+b)+c)+d).
131        let result = ["a", "b", "c", "d"]
132            .into_iter()
133            .map(String::from)
134            .reduce_balanced(|a, b| format!("({a}+{b})"));
135        assert_eq!(result, Some("((a+b)+(c+d))".to_string()));
136    }
137
138    #[test]
139    fn test_balanced_structure_odd() {
140        // [a, b, c] should produce ((a+b)+c) — odd element merges into last pair.
141        let result = ["a", "b", "c"]
142            .into_iter()
143            .map(String::from)
144            .reduce_balanced(|a, b| format!("({a}+{b})"));
145        assert_eq!(result, Some("((a+b)+c)".to_string()));
146    }
147
148    #[test]
149    fn test_balanced_structure_five() {
150        // [a, b, c, d, e] => ((a+b)+((c+d)+e))
151        let result = ["a", "b", "c", "d", "e"]
152            .into_iter()
153            .map(String::from)
154            .reduce_balanced(|a, b| format!("({a}+{b})"));
155        assert_eq!(result, Some("((a+b)+((c+d)+e))".to_string()));
156    }
157
158    #[test]
159    fn test_non_clone_items() {
160        #[derive(Debug, PartialEq, Eq)]
161        struct NonClone(String);
162
163        let result = ["a", "b", "c"]
164            .into_iter()
165            .map(|value| NonClone(value.to_owned()))
166            .reduce_balanced(|NonClone(lhs), NonClone(rhs)| NonClone(format!("({lhs}+{rhs})")));
167
168        assert_eq!(result, Some(NonClone("((a+b)+c)".to_owned())));
169    }
170
171    #[test]
172    fn test_try_reduce_balanced_ok() {
173        let result: Result<_, &str> = [1, 2, 3, 4]
174            .into_iter()
175            .try_reduce_balanced(|a, b| Ok(a + b));
176        assert_eq!(result, Ok(Some(10)));
177    }
178
179    #[test]
180    fn test_try_reduce_balanced_err() {
181        let result: Result<Option<i32>, &str> = [1, 2, 3, 4]
182            .into_iter()
183            .try_reduce_balanced(|a, b| if a + b > 4 { Err("too big") } else { Ok(a + b) });
184        assert_eq!(result, Err("too big"));
185    }
186
187    #[test]
188    fn test_try_reduce_balanced_empty() {
189        let result: Result<_, &str> =
190            std::iter::empty::<i32>().try_reduce_balanced(|a, b| Ok(a + b));
191        assert_eq!(result, Ok(None));
192    }
193
194    #[test]
195    fn test_try_reduce_balanced_single() {
196        let result: Result<_, &str> = [42].into_iter().try_reduce_balanced(|a, b| Ok(a + b));
197        assert_eq!(result, Ok(Some(42)));
198    }
199}