Writing a macro that contains a match body

2019-08-19 03:08发布

I'm attempting to condense some repetitive code that has structure similar to:

match self.foo() {
    None => self.bar(),
    Some(MyStruct { foo: x, .. }) => match x {
        Pattern1 => result,
        Pattern2 => {
            block_result
        }
    }
}

which I would like to write as something like:

my_macro!(
    Pattern1 => result,
    Pattern2 => {
        block_result
    }
)

avoiding the repetitive None handling and MyStruct destructuring.

This seems like it should be pretty simple, as it's essentially just substituting the macro body into a match expression, but I can't actually see any way to do this.

I attempted to write the macro as follows:

macro_rules! my_macro (
    ($($pat:pat => $result:expr,)*) => (
        match self.foo() {
            None => self.bar(),
            Some(MyStruct { foo: x, .. }) => match x {
                 $(
                     $pat => $result,
                 )*
            },
        }
    );
)

but this fails, as the RHS of a match arm can be an expression or a block (and it also doesn't deal with optionally omitting the comma for the last arm). As far as I'm aware there's no way to specify that part of a macro pattern can be a block or an expression, so I can't think of a way to resolve this.

Ideally I'd like to do this without having to write complicated patterns that destructure match bodies only to stick them back together again, but I don't think there's any designator that will accept the body of a match expression.

How would you go about writing this macro? Is it even possible without writing a compiler plugin?

1条回答
冷血范
2楼-- · 2019-08-19 03:17

I'm not sure why you decided that

this fails, as the RHS of a match arm can be an expression or a block

In Rust match arms are always expressions, and it just happened that blocks are also expressions.

There are two problems in your macro. First, as you noticed, your macro does not handle omitting last comma. This can be remedied very easily: you just change this pattern:

$($pat:pat => $result:expr,)*

into this one:

$($pat:pat => $result:expr),*

and its usage should be changed as well:

             $(
                 $pat => $result,
             )*

to

             $(
                 $pat => $result
             ),*

The second problem is that unless you define this macro in the scope which contains self identifier (i.e. inside the method), it won't work as you expect because of hygiene - the self identifier you use in self.foo() and self.bar() invocations in the macro body will be different from the one at the macro expansion site. As a general rule, you always need to pass identifiers you want to work with at the macro expansion site as arguments to the macro, unless this macro is defined in the scope where these identifiers are already present.

So, the final variant of the macro is this:

macro_rules! my_macro (
    ($_self:expr, $($pat:pat => $result:expr),*) => (
        match $_self.foo() {
            None => $_self.bar(),
            Some(MyStruct { foo: x, .. }) => match x {
                 $(
                     $pat => $result
                 ),*
            },
        }
    );
)

and it does work exactly as you want.

You can find more information on macros and how to write them in the official guide.

查看更多
登录 后发表回答