multi_try icon indicating copy to clipboard operation
multi_try copied to clipboard

The most ergonomic solution!

Open justforcomment opened this issue 1 year ago • 2 comments

The most ergonomic solution!

Hello, Josh!) I personally see this small crate as one of the best ways (even after 5 years) to manage errors (accumulating them all). There are alternatives: frunk, semval, garde, rust2fun, valid.

But the first one, of course, is very functional, non-idiomatic, and relatively complex. The next two, on the contrary, have infrastructural limitations for large-scale uniform application.

I really like the last two crates too.

The value of your approach is in its simplicity, clarity, and the ability to build a more special solution if necessary. And without the need to dive deeply into applicative functors and other Haskel twists and turns (although this is also interesting). Yes, Rust is not purely functional, so you have to add some magic to the impl_multi_try macro! - but its purpose is clear, even if you don’t have much experience writing macros. And this is your merit! Thank you =)

justforcomment avatar Aug 26 '24 02:08 justforcomment

Thank you for the kind words!

Credit for the macro based implementation goes to @sunjay and his generous PR #1.

JoshMcguigan avatar Aug 27 '24 02:08 JoshMcguigan

I join in the gratitude @sunjay !)

I understand that this topic may not be so relevant.. but still!)

One annoying limitation...

In general, it is much more likely that checking just one of the fields, for example, username, can already lead to several errors. Then the type for which we implement MultuTry will be Result<T, Vec<ERR>>.

However, it will conflict with other implementations for: Result<(A, B), Vec<ERR>>, Result<(A, B, C), Vec<ERR>> .. etc.

Example..

type StaticStr = &'static str;
impl<OT, T> MultiTry<OT> for Result<T, Vec<StaticStr>> {
    type Output = Result<(T, OT), Vec<StaticStr>>;
    fn and_try(self, other: Result<OT, Vec<StaticStr>>) -> Self::Output {
        unimplemented!()
    }
}

// Conflict implementation
impl<OT, A, B> MultiTry<OT> for Result<(A, B), Vec<StaticStr>> {
    type Output = Result<(A, B, OT), Vec<StaticStr>>;

    fn and_try(self, other: Result<OT, Vec<StaticStr>>) -> Self::Output {
        unimplemented!()
    }
}

As far as I understand, T simply includes all possible (A, B, ... Z). And there is no convenient way to express in a type the difference between the normal value of T and the value inside the tuple (A, B, ... Z).

In general, I would like to achieve the following (API inspired by Bpaf Rust-library):

pub type ValidationResult<T> = Result<T, Vec<StaticStr>>;

    fn parse_email(email: String) -> ValidationResult<Email> {
        AttrParser::build(email)
            .guard(
                |x| has_special_email_character(x),
                "No @ character",
            )
            .guard(
                |x| has_length_from_6_to_12(x),
                "Too short",
            )
            .parse(|x| Email(x))
    }

Where each guard call runs the passed predicate closure, after which the corresponding error value can be added to the message vector. And the final parse method returns Ok(// self.inner //) or Err(// self.errors (Vec<StaticStr>) //);

The parser function itself is defined inside the module of the corresponding Newtype structures in order to have access to the constructor. Then the client code will only have a single and safe way to create a Newtype value.

The limitation described above can be overcome by changing the implementation of the parse method. The return type of the parser function will become: fn parse_email(email: String) -> ValidationResult<(Email,)>;

Accordingly, you need to change the type in trait:

//before
fn and_try(self, other: Result<OT, Vec<StaticStr>>) -> Self::Output;

// after
fn and_try(self, other: Result<(OT, ), Vec<StaticStr>>) -> Self::Output;

And these calls can also be easily combined: let (a, b, d) = parse_email(_).and_try(parse_email(_).and_try(parse_something(_))?;

Yes. If you only need one value, then it might look like not very elegant, extra two parentheses, comma. Although.. Maybe Lisp fans will appreciate it).

let (a,) = parse_something(_)?;

// As in the parser function signature
fn parse_email(email: String) -> ValidationResult<(Email,)>;

Nevertheless, it remains quite simple and clear, in my opinion). Although perhaps there is a better way?)

justforcomment avatar Aug 27 '24 09:08 justforcomment