Pre-RFC: `Result::map_err_when`

I currently have this snippet:

self.next_u4().and_then(|magic| if magic == 0xCAFEBABE { Ok(magic) } else { Err(ParseError::InvalidMagic) })

Which maps the Result from self.next_u4() to an Err(ParseError::InvalidMagic) (if the magic doesn't match).

I currently feel that the current way of writing this is a bit too verbose, would the addition of a .map_err_when() method make sense?

Detailed Design

The .map_err_when() method takes in a closure and an error type E. The declaration would look like:

impl Result<T, E> {
    pub fn map_err_when<F>(self, pred: F, err: E) -> Result<T, E>
    where
        F: FnOnce(T) -> bool {
        /* implementation */
    }
}

The err parameter is the new E in the Result when the predicate returns true.

Not sure if map_err_when is the correct name though.

1 Like

Another name may be filter_or(), as a match for Option::filter().

4 Likes

FWIW, I don't think this needs an RFC. Small library additions like this generally can just be a PR against rust-lang/rust. They (if accepted) initially land as unstable and then eventually stabilize later, after an FCP.

(That said, my personal feelings are that this might be too niche, but have not thought deeply about it)

5 Likes

One day it might be

try {
    let magic = self.next_u4()?;
    if magic != 0xCAFEBABE {
        yeet ParseError::InvalidMagic;
    }
    magic
}
4 Likes

EDIT: Oops, these are not equivalent because doing the map_err separately loses information of a possible existing Err value. Mea culpa.

This can also be written as a .filter().map_err() chain which is probably how I’d write it.

2 Likes

The name filter feels wrong to me as I think that we aren't "filtering" out an element from a collection or something, we're just "mapping" into an error depending on the result of the predicate.

Do you have other examples of where this would be useful? If this is the only instance it does seem too niche.

And what will be the signature for filter()?

I’d say we’re definitely filtering the value (note that I’m talking about the already existing Result::filter method) but I realized a filter • map_err chain is not equivalent to your method after all because any existing Err value would be unconditionally overwritten which is not what you want.

AFAICT, there is no “already existing Result::filter method”.

Oh wow, brain fart. Sorry all :person_facepalming:

Hence my current idea of the addition of this method. A somewhat-equivalent to the Option::filter method.

1 Like

map_err_when sounds like it's related to map_err, which changes one Err(x) into a different Err(f(x)) and leaves Ok(_) untouched. Your operation here does neither of this, it conditionally changes Ok(_) into Err(e) and it leaves Err(x) untouched. Option's filter does conditionally change Some(_) into None and it leaves s None untouched, so that's a better fit for deriving the name from.

Of course the conditional would be reversed compared to your proposed map_error_when where a filter_or method would turn the value into Errwhen the condition closure returns false, not true.

W. r. t. naming, my initial. thoughts would be either filter_or/filter_or_else or filter/filter_with, where for each naming pair, the first method would be taking an error value, and the second one an error-producing closure.

Arguably, there should indeed really be a closure version, too, to avoid constructing unused error values that are too expensive.

That's then a method (self, impl FnOnce(&T) -> bool, impl FnOnce() -> E) -> Self or perhaps, since there's always an inner value T available when the second closure is called, a method (self, impl FnOnce(&T) -> bool, impl FnOnce(T) -> E) -> Self. [1]

OTOH... in this latter case, we have two closures with almost the same input producing first a bool, and then conditionally an E, so that might be better served by taking a single closure returning Result<T, E> (need to return the T because now it's an owned parameter even for determining whether or not to filter), though that then gives us the signature of and_then. In other words, we arrive back at the original question of whether or not writing foo.filter_or_else(|x| cond(x), |x| err_value(x)) as foo.and_then(|x| if cond(&x) { Ok(x) } else { Err(err_value(x)) } is too much effort. Or for foo.filter_or(|x| cond(x), e), there's also the option of writing foo.and_then(|x| cond(&x).then(|| x).ok_or(e)).


  1. Then, as a side-note, there's also the discussion of whether or not to make the first closure FnOnce(&mut T) -> bool. ↩︎

2 Likes

I definitely agree with that we arrive back at writing .and_then(..) if we take two closures.

I also support the idea that .filter_or(..) taking a closure and a specific error type, that it could be a useful "shorthand" over my aforementioned (maybe a bit niche) use case.

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