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

Added either::map_both, either:try_map_both #109

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

JohnScience
Copy link

@JohnScience JohnScience commented Sep 16, 2024

Added an additional line to the doc of either::for_both!:

/// Unlike [`map_both!`], this macro converges both variants to the type returned by the expression.

Implementation of map_both!

/// Evaluate the provided expression for both [`Either::Left`] and [`Either::Right`],
/// returning an [`Either`] with the results.
///
/// This macro is useful in cases where both sides of [`Either`] can be interacted with
/// in the same way even though the don't share the same type.
///
/// Syntax: `either::map_both!(` *expression* `,` *pattern* `=>` *expression* `)`
///
/// Unlike [`for_both!`], this macro returns an [`Either`] with the results of the expressions.
///
/// # Example
///
/// ```
/// use either::Either;
///
/// struct Wrapper<T>(T);
///
/// fn wrap(owned_or_borrowed: Either<String, &'static str>) -> Either<Wrapper<String>, Wrapper<&'static str>> {
///    either::map_both!(owned_or_borrowed, s => Wrapper(s))
/// }
/// ```
#[macro_export]
macro_rules! map_both {
    ($value:expr, $pattern:pat => $result:expr) => {
        match $value {
            $crate::Either::Left($pattern) => $crate::Either::Left($result),
            $crate::Either::Right($pattern) => $crate::Either::Right($result),
        }
    };
}

Implementation of try_map_both!:

/// Evaluate the provided expression for both [`Either::Left`] and [`Either::Right`],
/// returning a [Result] where the [`Ok`] variant is an [`Either`] with the results.
///
/// This macro is useful in cases where both sides of [`Either`] can be interacted with
/// in the same way even though the don't share the same type.
///
/// `either::map_both!(` *expression* `,` *pattern* `=>` *expression* `)`
///
/// Unlike [`map_both!`], this macro returns a [Result] where the [`Ok`] variant is an [`Either`] with the results.
///
/// # Example
///
/// ```
/// use either::Either;
///
/// fn wrap(owned_or_borrowed: Either<String, &'static str>) -> Result<Either<String, &'static str>, ()> {
///    either::try_map_both!(owned_or_borrowed, s => Ok(s))
/// }
/// ```
#[macro_export]
macro_rules! try_map_both {
    ($value:expr, $pattern:pat => $result:expr) => {
        match $value {
            $crate::Either::Left($pattern) => match $result {
                Ok(ok) => Ok($crate::Either::Left(ok)),
                Err(err) => Err(err),
            },
            $crate::Either::Right($pattern) => match $result {
                Ok(ok) => Ok($crate::Either::Right(ok)),
                Err(err) => Err(err),
            },
        }
    };
}

P.S.

If you have an idea for a simple and meaningful example for try_map_both!, it will be very welcome!

@JohnScience
Copy link
Author

@cuviper Ready for the review!

@JohnScience JohnScience changed the title Added either::map_both Added either::map_both, either:try_map_both Sep 16, 2024
@cuviper
Copy link
Member

cuviper commented Sep 16, 2024

This functionality already exists privately as map_either!:

either/src/lib.rs

Lines 133 to 140 in 53ae3de

macro_rules! map_either {
($value:expr, $pattern:pat => $result:expr) => {
match $value {
Left($pattern) => Left($result),
Right($pattern) => Right($result),
}
};
}

So the internal use is evidence enough that this is a useful macro. The existing macro is named like the map_either method, although that takes two separate mapping functions. Your map_both! is similar to the existing for_both!, where "both" is justified because a single $pattern => $result is applied to both variants. That seems fine to me, so we can change names.

Please do update the existing uses to map_both! and remove map_either!.

@cuviper
Copy link
Member

cuviper commented Sep 16, 2024

I'm less sure about your new addition of try_map_both! because it's limited to Result instead of generic Try, whereas you can probably just use map_both! with an ? operator inside the $result in many cases.

@JohnScience
Copy link
Author

JohnScience commented Sep 16, 2024

I'm less sure about your new addition of try_map_both! because it's limited to Result instead of generic Try, whereas you can probably just use map_both! with an ? operator inside the $result in many cases.

I'm not sure how to implement it properly given that https://doc.rust-lang.org/beta/std/ops/trait.Try.html is unstable.

whereas you can probably just use map_both! with an ? operator inside the $result in many cases.

And it is an amazing idea.

@JohnScience
Copy link
Author

I'll have to start working just in 3 minutes, so I'll probably be able to come back to it only in the evening!

Still, thank you a lot for the feedback!

@cuviper
Copy link
Member

cuviper commented Sep 16, 2024

I'm not sure how to implement it properly given that https://doc.rust-lang.org/beta/std/ops/trait.Try.html is unstable.

Right, you can't, which gives the advantage to the ? operator.

@JohnScience
Copy link
Author

I'm not sure how to implement it properly given that https://doc.rust-lang.org/beta/std/ops/trait.Try.html is unstable.

Right, you can't, which gives the advantage to the ? operator.

I reimplemented the code using a hidden and sealed Tryish trait, which mimics a part of the functionality of the Try trait, which is necessary to allow for try_map_both macro work as intended while Try trait is unstable.

Once Try trait is stabilized, we can switch to using Try trait instead of Tryish trait directly.

For the time being, it works with a Result and Option.

@JohnScience
Copy link
Author

@cuviper Ready for the second review!

@JohnScience
Copy link
Author

JohnScience commented Sep 18, 2024

There are a small annoyances that make me want to have a dedicated try_map_both! macro.

  • If we want to handle a certain subset of errors of the returned Result::Err, we'd have to have a closure/function that would capture the short-circuited with ? error.
  • If we write an application-level function with the return type of anyhow::Result<()> and call a function with a custom error, which we would like to handle, we would need to have a closure/function that would capture the short-circuited with ? error.
  • Under the hood, the point of converge of types to the result would be spread across the branches, which could create more problems for the optimization passes of LLVM.

@cuviper
Copy link
Member

cuviper commented Sep 18, 2024

Hidden closures in the macro also have costs, since that prevents control flow and some borrowck patterns that would otherwise be allowed in local code.

Have you considered map_both!(...).factor_err() for your try_map_both! use case?

@JohnScience
Copy link
Author

JohnScience commented Sep 18, 2024

Hidden closures in the macro also have costs, since that prevents control flow and some borrowck patterns that would otherwise be allowed in local code.

Where can I learn more about that problem? It feels more like an issue with the compiler than with the idea.

Have you considered map_both!(...).factor_err() for your try_map_both! use case?

And I guess it'll work but try_map_both! has the potential to support any Try type even before Try is stable.

@cuviper
Copy link
Member

cuviper commented Sep 18, 2024

Hidden closures in the macro also have costs, since that prevents control flow and some borrowck patterns that would otherwise be allowed in local code.

Where can I learn more about that problem? It feels more like an issue with the compiler than with the idea.

The control flow limitation is straightforward -- stuff like break, continue, and return can't reach outside the closure.

For borrowck, the biggest difference I can think of is with initialization. You can move locals into a closure capture without trouble, but you can't move them back to re-initialize the local, nor can you initialize it for the first time. Without a closure, borrowck is able to track discontinuous regions where a local is initialized.

@cuviper
Copy link
Member

cuviper commented Sep 18, 2024

FWIW, I do have a private Try in rayon too, but I think that has a much stronger justification for enabling parallel try_fold and such, which would be very difficult for users to accomplish otherwise. It also doesn't have the drawbacks about closures because it's not trying to do anything local like a macro.

Here, we're talking about a convenience macro for a relatively simple match, and I don't think the contortions and drawbacks are justified to emulate Try.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants