模块路径+类型名称的宏实例中应该使用哪些片段说明符(元变量类型)?

What fragment specifiers (metavariable types) should be used in macro-by-example for a module path + type name?

我发现自己写的代码是这样的:

// Of course I could add a use statement, but there's still undesireable duplication.
// in the real code theres anywhere from 3 to 10 items in the tuple,
// but that would just clog up this example
pub fn cols() -> (crate::foo::bar::A, crate::foo::bar::B) {
    (crate::foo::bar::A, crate::foo::bar::B)
}

很多次了。我试图创建一个宏来为我吐出这个函数:

macro_rules! impl_cols {
    ( $namespace:path, $($col_name:ty,)*) => {
        pub fn cols() -> ( $( $namespace::$col_name, )* ) {
            ( $( $namespace::$col_name, )* )
        }
    }
}

但无论我为元变量(pathidenttytt、它们的组合)选择什么片段说明符,它都会出错。让这个工作的魔法咒语是什么?

类型 AB 看起来像这样:

// In "foo.rs"

pub mod bar {
    pub struct A;
    pub struct B;
}

An (erroring) example on the rust playground.


这里当然有一个 XY 问题:我正在使用 diesel 板条箱并且具有带有 Queryable 派生 impl 的结构,我希望能够 .select() 正确的列来填充给定的结构。虽然可能还有其他解决方案,但我仍然想了解为什么我编写的宏不起作用,以及如果有什么会起作用。

你可以用 tt-munching 做到这一点:

macro_rules! impl_cols {
    (@build-tuple
        ( $($types:path,)* )
        ( $($ns:ident)::* )
        ( $col_name:ident, $($rest:tt)* )
    ) => {
        impl_cols! { @build-tuple
            (
                $($types,)* 
                $($ns::)* $col_name,
            )
            ( $($ns)::* )
            ( $($rest)* )
        }
    };
    // Empty case
    (@build-tuple
        ( $($types:path,)* )
        ( $($ns:ident)::* )
        ( )
    ) => {
        ( $($types,)* )
    };
    (
        $($ns:ident)::*,
        $($col_name:ident,)*
    ) => {
        pub fn cols() -> impl_cols! { @build-tuple
            ( )
            ( $($ns)::* )
            ( $($col_name,)* )
        } {
            impl_cols! { @build-tuple
                ( )
                ( $($ns)::* )
                ( $($col_name,)* )
            }
        }
    };
}

Playground.

编辑:

简单匹配不起作用的原因是您不能在 Rust 宏中连接路径(没有过程宏的帮助)。这是因为,引用参考文献 (https://doc.rust-lang.org/reference/macros-by-example.html#transcribing):

When forwarding a matched fragment to another macro-by-example, matchers in the second macro will see an opaque AST of the fragment type. The second macro can't use literal tokens to match the fragments in the matcher, only a fragment specifier of the same type. The ident, lifetime, and tt fragment types are an exception, and can be matched by literal tokens.

不仅在转发到另一个宏时如此,在转发到编译器时也是如此。

您想构建两个不同的 AST 片段:一个 Type for the return type (each type in the tuple), and an Expression for the body. That is, crate::foo::bar::A (for instance) fulfill two roles: in the return type it is a type (specifically TypePath), and in the body it is an expression (specifically PathExpression).

如果我们查看两者(TypePathPathExpression)的定义,我们会发现它们本质上等同于以下内容(忽略不相关的部分,如泛型和函数路径):

路径:
::? PathIdentSegment (:: PathIdentSegment) *

PathIdentSegment :
IDENTIFIER | super | self | Self | crate | $crate

如果您不熟悉 EBNF 表示法,这意味着标识符列表(macro_rules! 中的 :ident),由 :: 分隔。

因此,当您执行以下操作时:

macro_rules! concat_ns {
    ($ns:path, $type:ident) => {
        fn my_fn() -> $ns :: $type { todo!() }
    };
}
concat_ns!(crate::foo::bar, A)

您的宏调用构建了类似于以下内容的 AST:

MacroInvocation
  ...
    Path
      PathIdentSegment `crate`
      PathIdentSegment `foo`
      PathIdentSegment `bar`
    COMMA
    IDENTIFIER `A`

您的宏想要构建类似于以下内容的 AST:

Function
  ...
  FunctionReturnType
    Type
      Path
        <Insert metavariable $ns here>
        <Insert metavariable $type here>

这给你:

Function
  ...
  FunctionReturnType
    Type
      Path
        Path
          PathIdentSegment `crate`
          PathIdentSegment `foo`
          PathIdentSegment `bar`
        PathIdentSegment `A`

但这是一个无效的 AST,因为 Path 只能包含 PathIdentSegment 而不能包含其他 Path! (注意:这不是确切的过程,但大致相同)。

现在您也明白了为什么 tt-munching 解决方案有效:在那里,我们从不创建 Path 节点,只保留原始标识符。我们 可以 连接原始标识符并从中创建一个 Path (这通常是使用 tt-munchers 的原因:当我们不需要使用 Rust 宏的语法片段捕获能力时因为我们想在之后恢复它们)。