Implement `Sum` for `(T1, T2...) where T1: Sum, T2: Sum`

While this can be clearly implemented today adhoc with fold, I was wondering if this would be possible to add, so that one can do something like this:

let (total_points, total_health) = players.map(|p| (p.points, p.health)).sum();

And it would be element-wise addition. Similarly maybe product can have the same treatement?

let (points_iter, health_iter) = players.map(|p| (p.points, p.health)).unzip();
let total_points = points_iter.sum();
let total_health = health_iter.sum();

is an option

No, it isn't, unzip collects tuple elements rather than "forking" the iterator.

The problem is that this kind of implementation needs to accumulate the result element by element due to the lazy nature of iterators, but the interface of Sum only allows accumulating them all at once, so this is not possible to write, at least with the Sum trait we have today. unzip had the same problem and it solved it by using Default + Extend instead of FromIterator.

This doesn't compile because the type of points_iter and health_iter cannot be inferred. Moreover there's no way points_iter and health_iter can be lazy iterators because unzip has to consume the whole iterator it is given. So realistically their type will be Vec, which is too inefficient compared with the fold you could manually write.

1 Like

Hm, maybe I am misunderstanding the restriction you are mentioning, but I feel like this would be the kind of implementation that I expect from it: Rust Playground and it seems to work fine?

The type annotations are needed because println doesn't restrict it enough, but that's a problem that Sum has already.

Just for (T1, ...) instead of my custom type. (Since I can't extend a std type like that)


Re-reading your post, I am not sure why sum would be considered lazy? To me it's one of the 'terminators' of iteration, just like for_each, product, fold etc... Aka, a place where you realize the computation rather than just describing it. I am not aware of sum being seen as an abstract computation, and the type signature/impls support that idea, since you usually don't return a type that implements Iterator itself.

The title of your post claimed to implement Sum for (T1, T2, ...) where T1: Sum, T2: Sum, which is what I was answering to. You're right that by using Default and Add you can implement it though.

I didn't mean that sum should be considered lazy. What I meant is that because iterators are lazy (so you can't just iterate again, after they yield a value it's over), you have to add the new value to each accumulator (the one for T1, the one for T2 ecc ecc) before advancing the iterator. However, given the interface of Sum, you can only add all the values for T1 and only after think about T2, but at that point the iterator has already been consumed. Of course this argument doesn't hold when you use Add.

2 Likes

You're totally right! I meant that more in the general gist. My bad

Yup, that makes sense.

Welllllllllll, technically it is possible to evaluate the unzipped iterator on-demand. I managed to implement it fully generically. Obviously not something you want to do, but technically possible.

1 Like

Note that Default doesn't necessarily have the appropriate semantics for this— Nothing says that it should be the additive identity value. Depending on the type's expected usage, it could also be the multiplicative identity, a random sample from the type's domain, or anything else.

The trait you really want is num_traits::Zero.

1 Like

For the usage pattern of calling Sum::sum this will first have to consume the whole iterator (for the first Sum::sum), thus essentially collecting the rest of the items in the VecDeques but without optimizations like preallocations and such. I don't think this is better than unziping into Vecs.

It's certainly possible. I've created a quick macro to generate some impl. It's very hacky and I believe something like this but better should be apart of the stdlib.

pub trait TupleSum<A>: Sized {
    fn tuple_sum(self) -> A;
}
macro_rules! tuple_sum_impl {
    ($($y:ident + $b:ident =>$x:tt) +) => {
        impl <$($x: std::ops::Add<Output=$x> + std::default::Default),+,I:Iterator<Item= ($($x),+)>> TupleSum<($($x),+)> for I{
            fn tuple_sum(self) -> ($($x),+) {
                self.reduce(|($($y),+),($($b),+)|($($y +$b),+)).unwrap_or_default()
            }
        }
    };
}
tuple_sum_impl!(
    a + aa =>A
    b + bb =>B
);
tuple_sum_impl!(
    a + aa =>A
    b + bb =>B
    c + cc =>C
);
tuple_sum_impl!(
    a + aa =>A
    b + bb =>B
    c + cc =>C
    d + dd =>D
);
//etc
fn main(){
    let iter = vec![
        (1,2,1),
        (3,4,1),
        (5,6,1),
    ].into_iter();
    let t:(i32,i32,i32) = iter.tuple_sum();
    dbg!(t);
}

This was what you were looking for right?

You can actually get Zero::zero (the additive identity) with solely std, if you're willing to bend a few rules: it's spelled iter::empty()::sum(). One::one (the multiplicative identity) is spelled iter::empty().product(). :sweat_smile:

9 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.