rust - 如何在声明性宏中生成特征边界?

标签 rust type-bounds rust-decl-macros

我有一个具有大量关联类型的特征。我想要一个在 where 子句绑定(bind)的两侧使用这些关联类型的函数:

trait Kind {
    type A;
    type B;
    // 20+ more types
}

trait Bound<T> {}

fn example<K1, K2>()
where
    K1: Kind,
    K2: Kind,
    K1::A: Bound<K2::A>,
    K1::B: Bound<K2::B>,
    // 20+ more bounds
{
}
输入所有边界会有点脆弱,所以我想创建一个宏来生成这个:
fn example<K1, K2>()
where
    K1: Kind,
    K2: Kind,
    how_do_i_write_this!(K1, K2, Bound, [A, B, /* 20+ more types */])
{
}
但是,在 where 子句绑定(bind)的右侧调用宏会导致错误:
macro_rules! bound {
    () => { std::fmt::Debug };
}

fn another_example() 
    where
    u8: bound!(),
{}
error: expected one of `(`, `+`, `,`, `::`, `;`, `<`, or `{`, found `!`
 --> src/lib.rs:7:14
  |
7 |     u8: bound!(),
  |              ^ expected one of 7 possible tokens
是否有任何巧妙的宏技巧可以让我干掉这段代码?
我对宏更改的确切位置或参数没意见。例如,宏生成整个fn可以接受。
如果这是不可能的,我可以使用构建脚本,但如果可能,我宁愿将代码放在同一位置。

最佳答案

解决方案(TL,DR)

  • “发出”所需边界的宏
    macro_rules! with_generated_bounds {( $($rules:tt)* ) => (
        macro_rules! __emit__ { $($rules)* }
        __emit__! {
            K1: Kind,
            K2: Kind,
            K1::A: Bound<K2::A>,
            K1::B: Bound<K2::B>,
            // 20+ more bounds
        }
    )}
    
  • (下游)用户的 API
    with_generated_bounds! {( $($bounds:tt)* ) => (
        fn example<K1, K2>()
        where
            K1 : Kind,
            K2 : Kind,
            $($bounds)*
        { … }
    
        trait AnotherExample<K1 : Kind, K2 : Kind>
        where
            $($bounds)*
        { … }    
    )}
    

  • 解释
    这是 sk_pleasant's answer 的替代方案,他们正确地指出所有宏(包括程序宏,对于那些想知道的人),都有 a limited amount of allowed call sites .
  • 这种限制最著名的例子是 concat_idents! macro (或任何易于编写的程序宏 polyfill):虽然可以将宏扩展为(连接的)标识符,但不允许在 fn 之间调用宏。关键字和函数定义的其余部分,从而使 concat_idents!用于定义新函数(同样的限制使得这样的宏无法用于定义新类型等)。
    以及人们如何规避concat_idents!局限性?解决这个问题的最普遍的工具/ crate 是 ::paste ,带有同名的宏。
    宏的语法很特殊。而不是写:
    fn
    some_super_fancy_concat_idents![foo, bar]
    (args…)
    { body… }
    
    因为,正如我所提到的,这是不可能的,::paste::paste!的想法是在允许宏调用的地方调用,例如扩展到整个项目时,因此要求它包装整个函数定义 :
    outer_macro! {
        fn
        /* some special syntax here to signal to `outer_macro!` the intent
           to concatenate the identifiers `foo` and `bar`. */
        (args…)
        { body… }
    }
    
    例如。,
    ::paste::paste! {
        fn [< foo bar >] (args…) {
            body…
        }
    }
    
    当我们想到这一点时,由于外部宏将整个输入“代码”视为任意标记(不一定是 Rust 代码!),我们开始支持想象的语法,例如 [< … >] ,甚至是模仿(和伪造!)宏调用的语法,但实际上只是一个语法指示符,很像 [< … >]曾是。即,paste!的 API 可能是:
    imaginary::paste! { // <- preprocessor
        // not a real macro call,
        // just a syntactical designator
        // vvvvvvvvvvvvvvvvvvvvvvvv
        fn concat_idents!(foo, bar) (args…) { body… }
    }
    
    整个事情的两个关键思想是:
  • 通过使用包装整个函数定义(一个项目)的外部调用,我们可以避免担心宏调用站点 🙂
  • 我们还可以使用我们自己的任意语法和规则,例如伪宏。

  • 这些是的核心思想预处理器模式 .

    此时,类似paste! ,可以设想具有以下 API 的 proc-macro 方法:
    my_own_preprocessor! {
        #![define_pseudo_macro(my_bounds := {
            K1: Kind,
            K2: Kind,
            K1::A: Bound<K2::A>,
            K1::B: Bound<K2::B>,
            // 20+ more bounds
        })]
    
        fn example<K1, K2>()
        where
            K1: Kind,
            K2: Kind,
            my_bounds!() // <- fake macro / syntactical designator for `…preprocessor!`
        …
    
        trait AnotherExample<K1 : Kind, K2 : Kind>
        where
            my_bounds!() // <- ditto
        {}
    }
    
    这可以做到,但实现辅助程序宏( my_own_preprocessor! )并非易事。

    还有另一种类似于预处理器模式的方法,但在这种情况下,它更容易特征化。它是 宏观目标
    回调/继续传递风格
    (CPS) 图案 .这种模式目前不时出现,但有点麻烦。这个想法是,我们希望“发出”而不是发出的 token 被传递给另一个宏——一个由调用者提供的!——它最终负责处理这些 token 并发出有效的宏扩展——例如一堆项目/功能——相应地。
    例如,考虑做:
    macro_rules! emit_defs {(
        $($bounds:tt)*
    ) => (
        fn example<K1, K2>()
        where
            K1 : Kind,
            K2 : Kind,
            $($bounds)*
        { … }
    
        trait AnotherExample<K1 : Kind, K2 : Kind>
        where
            $($bounds)*
        { … }
    )}
    
    generate_bounds!(=> emit_defs!);
    
    如果这看起来是一个笨拙但可以接受的 API,那么您应该知道实现 generate_bounds! 的主体 super 简单!确实,它只是:
    macro_rules! generate_bounds {(
        => $macro_name:ident !
        /* Optionally, we could try to support a fully qualified macro path */
    ) => (
        $macro_name! {
            K1::A: Bound<K2::A>,
            K1::B: Bound<K2::B>,
            // 20+ more bounds
        }
    )}
    
    将此与我们宏的天真定义进行比较:
    macro_rules! generate_bounds {() => (
        K1::A: Bound<K2::A>,
        K1::B: Bound<K2::B>,
        // 20+ more bounds
    )}
    
    唯一的区别是我们将一个宏(它将被提供给我们返回的“值”)作为输入,并且我们将我们的“返回”代码包装在它的调用中。
    在这一点上,我建议暂停并凝视前面的片段。基于回调的模式的概念简单(即使嘈杂)和强大的功能通常非常出色,这也不异常(exception)!

    这已经很好了,并且已经是一个有时可以在 Rust 生态系统中发现的解决方案。
    但是,恕我直言,这还不够好:用户的人体工程学非常糟糕。为什么调用者要经历定义帮助宏的所有麻烦,这可能会中断定义他们想要定义的函数的流程?那个宏应该如何命名?真的无所谓,火了忘了“回调”宏!
  • 我们遇到的问题与那些必须在 C 中定义回调(甚至是无状态的)的问题非常相似:而不是编写
    with(iterator, |each_element: ElementTy| {
        …
    });
    
    当时,C 必须写一些与 Rust 相同的东西:
    fn handle_element(each_element: ElementTy) {
        …
    }
    
    with(iterator, handle_element);
    
    对比一下我们的情况:
    macro_rules! handle_bounds {( $($bounds:tt)* ) => (
        fn example…
        where
            $($bounds)*
        …
    )}
    
    generate_bounds!(=> handle_bounds!);
    

  • 从这里,很容易想出所需的 API。类似的东西:
    with_generated_bounds! {( $($bounds:tt)* ) => (
        fn example…
        where
            $($bounds)*
        …
    )}
    
    并且从“命名回调”一个(=> macro_name! 一个)中使用这个 API,实际上非常简单:如果我们盯着前面的两个片段,我们可以注意到调用者提供的“回调”正是主体的 macro_rules!定义。
    因此,我们可以自己(被调用者)定义“助手”宏,使用调用者提供的规则,然后在我们希望发出的代码上调用这个助手宏。
    这导致了本文开头介绍的解决方案(为方便起见重复了🙃):
  • “发出”所需边界的宏
    macro_rules! with_generated_bounds {( $($rules:tt)* ) => (
        /// The helper "callback" macro
        macro_rules! __emit__ { $($rules)* }
    
        __emit__! {
            K1: Kind,
            K2: Kind,
            K1::A: Bound<K2::A>,
            K1::B: Bound<K2::B>,
            // 20+ more bounds
        }
    )}
    
  • (下游)用户的 API
    with_generated_bounds! {( $($bounds:tt)* ) => (
        fn example<K1, K2>()
        where
            K1 : Kind,
            K2 : Kind,
            $($bounds)*
        { … }
    
        trait AnotherExample<K1 : Kind, K2 : Kind>
        where
            $($bounds)*
        { … }    
    )}
    

  • 瞧🙂
    在采用实际宏参数时是否采用这种模式?
    例如,上述示例是硬编码名称 K1, K2 .将这些作为参数怎么样?
  • 用户 API 应遵循以下原则:
    with_bounds_for! { K1, K2, ( $($bounds:tt)* ) => (
        fn example<K1, K2>()
        where
            $($bounds)*
        …
    )}
    
  • 内联回调模式宏将是:
    macro_rules! with_bounds_for {(
        $K1:ident, $K2:ident, $($rules:tt)*
    ) => (
        macro_rules! __emit__ { $($rules)* }
        __emit__! {
            $K1 : Kind,
            $K2 : Kind,
            …
        }
    )}
    

  • 一些备注
    注意with_generated_bounds!的扩展是:
  • 宏定义;
  • 宏调用。

  • 这是两个“语句”,这意味着宏的整个扩展本身就是一个“语句”,这意味着以下内容不起作用:
    macro_rules! with_42 {( $($rules:tt)* ) => (
        macro_rules! __emit__ { $($rules)* }
        __emit__! { 42 }
    )}
    
    //      this macro invocation expands to two "statements";
    //      it is thus a statement / `()`-evaluating expression itself
    //      vvvvvvvvvv
    let x = with_42! {( $ft:expr ) => (
        $ft + 27
    )};
    
    这是nihil novi sub鞋底/太阳底下无新事;这与以下问题相同:
    macro_rules! example {() => (
        let ft = 42; // <- one "statement"
        ft + 27      // <- an expression
    )}
    
    let x = example!(); // Error
    
    在这种情况下,解决方案很简单:将语句括在大括号中,以便发出一个块,从而可以计算出它的最后一个表达式:
    macro_rules! example {() => ({
        let ft = 42;
        ft + 27
    })}
    
  • (顺便说一句,这就是我更喜欢使用 => ( … ) 作为 macro rules 的右手边的原因;它比 => { … } 更不容易出错/犯错)。

  • 在这种情况下,相同的解决方案适用于回调模式:
    macro_rules! with_ft {( $($rules:tt)* ) => ({
        macro_rules! __emit__ { $($rules)* }
        __emit__! { 42 }
    })}
    // OK
    let x = with_ft! {( $ft:expr ) => (
        $ft + 27
    )};
    
    这使得宏为 expr -友好,但代价是导致项目定义的作用域块:
    // Now the following fails!
    with_ft! {( $ft:expr ) => (
        fn get_ft() -> i32 {
            $ft
        }
    )}
    get_ft(); // Error, no `get_ft` in this scope
    
    确实,get_ft的定义现在范围在大括号内 😕
    因此,这是内联/匿名回调模式的主要限制:虽然它足以模拟“任意扩展”和“任意调用点”,但仅限于必须事先选择是否将宏定义包装在一个大括号内阻止与否,这使其与表达式扩展宏或公共(public)项目扩展宏兼容。在这方面,本文中间介绍的稍微麻烦的命名回调模式(=> macro_name! 语法)没有这个问题。

    关于rust - 如何在声明性宏中生成特征边界?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68728105/

    相关文章:

    未明确指定类型参数时,Scala 下限类型参数不起作用

    scala - 为什么 "K <: T"中的T不能协变?

    rust - 宏中的元组索引

    enums - 为什么这个 Rust 枚举没有变小?

    rust - f32 没有实现减法?

    vector - 为什么通过替换默认值填充 Vec 比填充具有预设容量的东西快得多?

    scala - Scala 中类型类的动机是什么?

    rust - 如何实现定义新公共(public)类型并返回该类型实例的宏?

    iterator - 从函数返回的迭代器的生存期要求冲突