rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.93k stars 1.57k forks source link

Proposal for a macro like `matches!()` that maps extracted value to `Option` #2960

Open Boscop opened 4 years ago

Boscop commented 4 years ago

After the inclusion of the matches macro into std, there are still a lot of similar use cases that it can't be used with: Those that require extracting a value like this:

    match param {
        ParamSelector::Int(_, TrackParam::Ext(name)) => Some(name),
        _ => None,
    }

These patterns occur frequently (or variations with if-let). There is currently no concise one-line way to write this. So I adapted the matches macro to work for these use cases:

#[macro_export]
macro_rules! match_map {
    ($expression:expr, $( $pattern:pat )|+ $( if $guard: expr )? => $ret:expr) => {
        match $expression {
            $( $pattern )|+ $( if $guard )? => Some($ret),
            _ => None
        }
    }
}

enum E { A(i32), B(i32), C }

fn main() {
    assert_eq!(match_map!(E::A(21), E::A(x) | E::B(x) => x * 2), Some(42));
}

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=a3df83eee3d96f05233d78d00f7bc801

I use this macro as often as matches and think it would be useful to have it in std.


Compared to using the more verbose match/if-let way, this macro also has the benefit of readability, when the value is post-processed using .map(..)/.and_then(..)/.ok_or_else(..) or similar Option methods.

Compare:

    (match param {
        TrackParam::Ext(name) => Some(name),
        _ => None,
    }).and_then(|name| func(name))

and:

    match_map!(param, TrackParam::Ext(name) => name).and_then(|name| func(name))
Lokathor commented 4 years ago

Maybe put this in a crate for a while and see if it gets picked up.

shepmaster commented 4 years ago

I’m generally in favor. I tend to create similar macros for tests.

I also create derive macros (Decompose) to produce this as an inherent method.

burdges commented 4 years ago

We might pursue the field puns color with a proc macro that extracts variant's fields based upon names?

enum Foo {
    A(..),  // Ignore all tuple variants because .0, etc. rarely turn out consistent enough
    B { x: u32, y: u32 },
    C { x: u16, y: u32 },
}

fn foo(f: Foo) {
    variant_field! { f; f.x }  // Error, x has inconsistent types
    variant_field! { f; u32::from(f.x) }  // Ok, the code inside resolves in all cases
    variant_field! { f; f.y }  // Ok, all y have consistent types
}

An easier syntax might be

    variant_field! { f { x, ..}; bar(x) }  // Error, x has inconsistent types

It'd desugar as applying this binding {x, ..} to all variants with those fields and returning Some(..), or _ => None.

scottmcm commented 4 years ago

Another take on this: bikeshed!(AAA => BBB) => if AAA { Some(BBB) } else { None}. Then you'd have let opt1 = bikeshed!( let Foo::B { x, .. } = foo => x ) but also other things like let opt2 = bikeshed!(x > 3 => 4);.

Of course, I've also pondered before changing the implicit else { () } to else { SomeTrait::method() } so that this would work with just normal if. Shouldn't even cause inference problems, since you need to be producing a () from the if anyway today...

yvt commented 3 years ago

My try_match crate provides something similar to that. I personally find this quite useful as a shortcut in some situations. Here are some examples:

tcw3/designer/src/metadata.rs

    pub fn field(&self) -> Option<&FieldDef> {
        try_match!(Self::Field(field) = self).ok()
    }

support/leakypool/src/lib.rs

        // Update `first_free` to point to the next vacant entry (if there's one).
        // This `unsafe` is safe because all entires in the linked list
        // `self.first_free` are supposed to be vacant.
        let next_ptr = try_match!(EntryState::Vacant(next_ptr) = old_state)
            .unwrap_or_else(|_| unsafe { unreachable_unchecked() });

This macro evaluates to Ok(matched_things) on successful match and Err(original_value) otherwise. I haven't found any wild or my usages that use the Err variant, so returning Option will probably suffice in practice, though Result could be useful to augment the unwrap panic message.