1

I have a function float_or_err(i) that takes an integer and returns either a float or an error. I want to write a function that takes an array of integers and returns the one for which float_or_err(i) is greatest, or propagates the error if float_or_err() errors on any of the is.

Now, I have read Using max_by_key on a vector of floats [duplicate] and I understand why Rust doesn't implement a total ordering on floats, and I have decided to use the ordered_float crate instead of f64 to impose a total ordering on the results of float_or_err().

However, that question does not address the case where the argument to .max_by_key() is a closure that returns a Result<OrderedFloat<f64>, _>, as in the following example:

use ordered_float::OrderedFloat;

fn main() {}

enum ErrorKind {
    ValidationError
}

// Function of `i` that normally returns an f64 but sometimes has an error
fn float_or_err(i: &isize) -> Result<OrderedFloat<f64>, ErrorKind> {
    if i % 5 == 0 {
        return Err(ErrorKind::ValidationError);
    }
    else {
        return Ok(OrderedFloat(3.14));
    }
}

// Given a vec of integers `i`, find the one for which `float_or_err(i)` is 
// largest. If any of those integers `i` yields an error in `float_or_err(i)`,
// return the error.
fn max_by_float_or_err(v: Vec<isize>) -> Option<&isize> {
    return v.iter().max_by_key(|i| float_or_err(i));
}

The compiler does not accept the second-to-last line, because

the trait bound `ErrorKind: Ord` is not satisfied
the trait `Ord` is implemented for `Result<T, E>`

OK, looks like I need to do something like

impl Ord for Result<OrderedFloat<f64>, ErrorKind> {
    // ...
}

(where I order first by the float, then use the ErrorKind as a tiebreaker.)

But the compiler won't let me implement Ord for a type that I didn't create:

`Result` is not defined in the current craterustcE0117
main.rs(9, 1): original diagnostic
only traits defined in the current crate can be implemented for types defined outside of the crate

Actually, my first intuition is to write:

fn argmax_by_float_or_err(v: Vec<isize>) -> Option<&isize> {
    return v.iter().max_by_key(|i| float_or_err(i)?);
}

But this doesn't compile either, because you can't use the question mark operator inside of a closure. I have read Alternatives for using the question mark operator inside a map function closure, where the suggested solution is to just to move the question mark to the end of the chain, but that solution is particular to the function in question, where the reduce op is .sum() instead of .max_by_key(), and .sum() is implemented natively for Result. This is not the case for .max_by_key() (and many other reduce ops).

  • In the context of the max_by_float_or_err() function defined above, how can I propogate errors from float_or_err()?
  • I am looking for a solution that uses .max_by_key() (or at least another reduce op) instead of rewriting the body as a for loop.

Edit: I also read How do I implement a trait I don't own for a type I don't own?, which suggests something like:

struct MyResult(Result<OrderedFloat<f64>, ErrorKind>);

impl Ord for MyResult {
    // ...
}

This solution is unsatisfactory because instances of MyResult don't seem to inherit all the methods associated with normal Results. For example, I cannot write:

let my_result: OrderedFloat<f64> = float_or_err(-1).unwrap_or_default(6.28);
Max
  • 695
  • 5
  • 18
  • 1
    By the way, the signature `(v: Vec) -> Option<&isize>` won't work since it would attempt to return a reference to a value `v` that would be destroyed at the end of the function. You probably want `v: &[isize]` (as the answers assume) or `.into_iter()` + `Option`. – kmdreko May 29 '23 at 16:15

2 Answers2

3

The issue here is that you are looking to short-circuit on an Err, which the max_by_key function is not capable of. I would probably make this in a for loop, but this is possible using a short-circuiting method such as try_fold.

pub fn max_by_float_or_err(v: &[isize]) -> Result<Option<&isize>, ErrorKind> {
    let Some((first, rest)) = v.split_first() else { return Ok(None) };
    let initial = (first, float_or_err(first)?);

    let max: Result<&isize, ErrorKind> = rest
        .iter()
        .try_fold(initial, |(acc, acc_key), item| {
            let item_key = float_or_err(item)?;
            if acc_key > item_key {
                Ok((acc, acc_key))
            } else {
                Ok((item, item_key))
            }
        })
        .map(|(item, _key)| item);

    Some(max).transpose()
}

In the future, this would be slightly simpler with try_reduce

drewtato
  • 6,783
  • 1
  • 12
  • 17
  • How would I set the value of `initial` dynamically if I have a filter between `.iter()` and `.try_fold()`? Is there a way to do this without creating two iters? I think this convinces me that I should just use a for loop :) – Max May 29 '23 at 16:46
  • @Max You can either apply the filter to the first element before passing it into `filter`, or you can create the iterator with filter first and use `next` instead of `split_first`. – drewtato May 29 '23 at 21:17
2

Here's a generic version of max_by_key() that propagates errors:

fn try_max_by_key<I, F, K, E>(mut iter: I, mut f: F) -> Result<Option<I::Item>, E>
where
    I: Iterator,
    F: FnMut(&I::Item) -> Result<K, E>,
    K: Ord,
{
    let Some(mut curr) = iter.next() else { return Ok(None) };
    let mut curr_key = f(&curr)?;

    for next in iter {
        let next_key = f(&next)?;
        if next_key > curr_key {
            curr = next;
            curr_key = next_key;
        }
    }

    Ok(Some(curr))
}
kmdreko
  • 42,554
  • 6
  • 57
  • 106