Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iterator's .sum() and .product() should mention that they work on iterators over Result<T,E> and Option<T> #105266

Closed
mday64 opened this issue Dec 4, 2022 · 0 comments · Fixed by #106434
Labels
A-docs Area: documentation for any part of the project, including the compiler, standard library, and tools

Comments

@mday64
Copy link

mday64 commented Dec 4, 2022

Summary

It would be helpful if Iterator's .sum() method would explicitly document that it can sum an iterator of Result<T,E> and produce a Result<T,E> as output, and that it stops consuming the iterator when it hits the first error. To compound things, my mental model was a little off, and I didn't understand the compiler's diagnostics well enough to get nudged in the right direction.

Likewise, it would be helpful to also document summing an Option<T>. Similarly, for .product() and Result<T,E> or Option<T>. I'm not sure if there are other Iterator methods that also handle Result and Option similarly.

Steps to Reproduce

If I go to the standard library documentation at https://doc.rust-lang.org/std/index.html, and then search for sum, then click on std::iter::Iterator::sum it gives an example of summing some integers. It would be helpful if this part of the documentation mentioned that you can also sum an iterator of Result<T,E>, which produces a Result<T,E> where the Ok() variant contains the sum (if there were no errors), or the Err() variant contains the first error. It would also be helpful to mention that it stops consuming the iterator once it encounters the first error.

If I click on Sum in the declaration, it takes me to documentation of the Sum trait. In the Implementors section, there is a long list of implementations for various numeric types, their Wrapping variants, their references, Wrapping variants of references, SIMD stuff. At that point, my eyes glazed over, and I missed this one buried near the end:

	impl<T, U, E> Sum<Result<U, E>> for Result<T, E>
	where
	    T: Sum<U>

If I click the "source" link to the right of the above impl, the doc comments describe the behavior, and there is even an example (yay!). As far as I can tell, those doc comments aren't rendered on the documentation page for either the Sum trait, or the .sum() method. It would probably be helpful to render them on the page for the Sum trait, and add a sentence or two to the documentation for .sum() (perhaps link to the Sum documentation if you don't want to repeat it all).

Background

This came about from the Advent of Code 2022, Day 1. See https://adventofcode.com/2022/day/1. The input is some text with groups of integers. The groups are separated by blank lines. Start by reading the input:

    let input = std::fs::read_to_string("src/bin/day01/input.txt")
        .expect("Can't open input.txt");

Now build a Vec where each element is the sum of the numbers in one group:

    let mut elf_totals: Vec<u32> = input.split("\n\n").map(|s|
        // s is the numbers for one elf
        s.split_terminator('\n').map(|n|
            // n is one number for the current elf
            n.parse::<u32>().expect("invalid number")
        ).sum()
    ).collect();

That works, but I'd like to do a better job of handling errors. First step is to change the function (main in this case) to return a Result<(), Box<dyn Error>>, and add an OK(()) at the end of the function.

Now, let's replace the .expect(...) with a question mark. My intent (for now) is to return from the function, not just the closure. I'm using VSCode with rust-analyzer. In the Problems tab, it says:

    the `?` operator can only be used in a closure that returns `Result` or `Option`...
    the trait `FromResidual<Result<Infallible, ParseIntError>>` is not implemented for `u32`
    main.rs[Ln 8, Col 38]: this function should return `Result` or `Option` to accept `?`

My first thought is that my function (main) does return Result. Oh, that last message is talking about the closure I'm passing to .map(). OK, let's try return instead:

            let x = n.parse::<u32>();
            if let Err(e) = x {
                return Err(e);
            } else {
                x.unwrap()
            }

Lots of errors. First one says "closure bodies that contain statements must be surrounded by braces." I can do that (there's even a Quick Fix that adds them for me!). Now there are only 3 errors:

	the trait bound `u32: Sum<Result<_, ParseIntError>>` is not satisfied
	the following other types implement trait `Sum<A>`:
		...
	expected Result<{unknown}, ParseIntError>, found u32
	mismatched types
	expected enum `Result<_, ParseIntError>`
	found type `u32`

For that last error, it suggests wrapping the expression in Ok(). Let the Quick Fix do that. It wrapped the entire if-else inside Ok(). That still leaves the first error message:

	the trait bound `u32: Sum<Result<_, ParseIntError>>` is not satisfied
	the following other types implement trait `Sum<A>`:
		...

It never occurred to me that the closure could return a Result. After all, I'm trying to sum integers, so I assume my closure has to return an integer. That return statement I just added is returning from the closure, not the function. I go off to Google ways to return from a function from within a closure. I didn't find anything that looked promising.

I take a different approach. Instead of using nested closures, I use nested for loops. Since it's not inside a closure any more, I can use the question mark to return from the function:

    let mut elf_totals: Vec<u32> = Vec::new();
    for group in input.split("\n\n") {
        let mut total: u32 = 0;
        for line in group.lines() {
            total += line.parse::<u32>()?;
        }
        elf_totals.push(total);
    }

That works, but there's got to be a better way. I figured out how to write a function that sums an iterator of Result<T,E> and produces a Result<T,E>, and short circuits on the first error. I didn't know that .sum() could already do this.

fn try_sum<I,T,E>(iter: I) -> Result<T,E>
    where T: Default + AddAssign, I: Iterator<Item=Result<T,E>>
{
    let mut result = T::default();
    for x in iter {
        result += x?;
    }
    Ok(result)
}

I figured out how to turn that into a generic trait so I could use it as a method (like the methods in the Itertools trait). That took a bunch of fiddling with generics and bounds, but I was able to work through it (in large part because the diagnostics nudged me in the right direction). The final piece was figuring out how to write the impl block so I could replace .sum() with my new .try_sum() method.

trait TrySum: Iterator {
    fn try_sum<T,E>(&mut self) -> Result<T,E>
    where   Self: Iterator<Item=Result<T,E>>,
            T: Default + AddAssign,
    {
        let mut answer = T::default();
        for x in self {
            answer += x?;
        }
        Ok(answer)
    }
}

impl<T> TrySum for T where T: Iterator {}

I knew that .collect() could transform an iterator over Result<T,E> into Result<Vec<T>, E>, and therefore I can put a question mark or .expect() after .collect().

    let mut elf_totals = input.split("\n\n").map(|s| {
        // s is the numbers for one elf
        s.split_terminator('\n').map(|n|
            // n is one number for the current elf
            n.parse::<u32>()
        ).try_sum()
    }).collect::<Result<Vec<u32>,_>>()?;

My implementation required the type I'm summing to implement Default and AddAssign. Can I get rid of the need for Default? Should I depend on Add instead of AddAssign? I start digging through the implementation of .sum() in the standard library to see if it does anything special that my implementation should. I ended up finding that sum (and other methods, like product) share a general purpose mechanism to handle iteration over Result<T,E>. That means that .sum() already does what my .try_sum() does. I replace .try_sum() with .sum() and ... it works!

@mday64 mday64 added the A-docs Area: documentation for any part of the project, including the compiler, standard library, and tools label Dec 4, 2022
Dylan-DPC added a commit to Dylan-DPC/rust that referenced this issue Mar 21, 2023
…he8472

Document `Iterator::sum/product` for Option/Result

Closes rust-lang#105266

We already document the similar behavior for `collect()` so I believe it makes sense to add this too. The Option/Result implementations *are* documented on their respective pages and the page for `Sum`, but buried amongst many other trait impls which doesn't make it very discoverable.

`@rustbot` label +A-docs
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Mar 21, 2023
…he8472

Document `Iterator::sum/product` for Option/Result

Closes rust-lang#105266

We already document the similar behavior for `collect()` so I believe it makes sense to add this too. The Option/Result implementations *are* documented on their respective pages and the page for `Sum`, but buried amongst many other trait impls which doesn't make it very discoverable.

``@rustbot`` label +A-docs
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Mar 21, 2023
…he8472

Document `Iterator::sum/product` for Option/Result

Closes rust-lang#105266

We already document the similar behavior for `collect()` so I believe it makes sense to add this too. The Option/Result implementations *are* documented on their respective pages and the page for `Sum`, but buried amongst many other trait impls which doesn't make it very discoverable.

```@rustbot``` label +A-docs
Nilstrieb added a commit to Nilstrieb/rust that referenced this issue Mar 21, 2023
…he8472

Document `Iterator::sum/product` for Option/Result

Closes rust-lang#105266

We already document the similar behavior for `collect()` so I believe it makes sense to add this too. The Option/Result implementations *are* documented on their respective pages and the page for `Sum`, but buried amongst many other trait impls which doesn't make it very discoverable.

````@rustbot```` label +A-docs
@bors bors closed this as completed in caae551 Mar 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-docs Area: documentation for any part of the project, including the compiler, standard library, and tools
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant