Validation monad

I'm looking for something that

  • resembles Result in that it offers the choice between success and well-described-failure,
  • is unlike Result in that (in concert with language and library features), it collects all the failures encountered in some process, rather than bailing at the first sign of trouble.

Can you think of any prior art along these lines in Rust?

[aside:

In Haskell this sort of thing tends to be called a validation monad:

  • Validation? Think of validating a web form; you'd like to be given a summary of all the problematic entries, rather than just the first one.

  • Monad? It allows composition of computations of some kind into larger ones of the same kind, where 'kind', in this case, refers to "either success or a list of all failures".

]

I don't know of anything like this in the Rust ecosystem, but the FromIterator impl for Item = Result does, I think, allow to partition/collect into 2 collections of Ok and Err

fn partition_results<T, E, S, F>(iter: impl Iterator<Item = Result<T, E>>) -> (S, F)
where
    T: std::fmt::Debug,
    E: std::fmt::Debug,
    S: FromIterator<T>,
    F: FromIterator<E>,
{
    let (oks, errs) = iter.partition::<Vec<_>, _>(Result::is_ok);
    (
        oks.into_iter().map(Result::unwrap).collect(),
        errs.into_iter().map(Result::unwrap_err).collect(),
    )
}

Making this "lazier" would be pretty easy hmm

1 Like

Unfortunately only the success variant can be accumulated into something that implements FromIterator: Result in std::result - Rust. But I agree that Result<T, impl Iterator<Item = E>> sounds quite close to what OP is looking for.

-> Result<T, impl Iterator<Item = E>> is simple for the caller to use, but how do you implement it? This is what comes to mind, and it isn't pretty:

fn validate_num(num: i64) -> Result<i64, impl Iterator<Item = String>> {
    let e1 = (num % 2 != 0).then(|| format!("{num} is not even"));
    let e2 = (num > 0).then(|| format!("{num} is negative"));
    combine(num, e1.into_iter().chain(e2))
}

fn combine<T>(value: T, errors: impl Iterator<Item = String>) -> Result<T, impl Iterator<Item = String>> {
    let mut errors = errors.peekable();
    if errors.peek().is_none() {
        Ok(value)
    } else {
        Err(errors)
    }
}

Itertools, not std, has the method I was thinking of: partition_result

Stop thinking of these as failures and your code structure is going to work out better.

Phrase it as a validator where success is producing the messages about the things it correctly found in the input.

My favourite way is to take a &mut impl FnMut(Message) in the relevant places, and all the messages get sent to that side channel. Then you have the option of passing a closure than panics, one that ignores them, one that puts them in a Vec, one that sends them through a channel, whatever. And if you don't want multiple mono copies you can instantiate it with dyn FnMut.

5 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.