#local #closures #callback #cps #with #proc-macro

with_locals

函数属性,通过使用CPS返回局部变量的引用

16个版本

0.3.3 2024年8月10日
0.3.2 2023年7月3日
0.3.1 2023年3月24日
0.3.0 2021年9月10日
0.0.1-alpha-22020年9月1日

213 in Rust模式

Download history 12090/week @ 2024-05-03 14599/week @ 2024-05-10 15171/week @ 2024-05-17 15714/week @ 2024-05-24 17871/week @ 2024-05-31 12644/week @ 2024-06-07 13231/week @ 2024-06-14 16748/week @ 2024-06-21 14579/week @ 2024-06-28 7622/week @ 2024-07-05 6849/week @ 2024-07-12 7762/week @ 2024-07-19 6615/week @ 2024-07-26 6121/week @ 2024-08-02 6345/week @ 2024-08-09 7408/week @ 2024-08-16

28,256 monthly downloads
用于 stackbox

Zlib OR MIT OR Apache-2.0

28KB
221

::with_locals

Repository Latest version Documentation MSRV unsafe forbidden License CI

Rust中的CPS糖,用于“返回”指向局部变量的值。

让我们从一个基本示例开始:返回/yielding一个 format_args 局部变量。

use ::core::fmt::Display;
use ::with_locals::with;

#[with('local)]
fn hex (n: u32) -> &'local dyn Display
{
    &format_args!("{:#x}", n)
}

上面的代码变为

use ::core::fmt::Display;

fn with_hex <R, F> (n: u32, f: F) -> R
where           F : FnOnce(&'_     dyn Display) -> R,
 // for<'local> F : FnOnce(&'local dyn Display) -> R,
{
    f(&format_args!("{:#x}", n))
}

f: F,这里被称为延续:函数不是返回/产生一些元素/对象,而是接收调用者希望对该元素(一旦接收到它)执行的操作的“逻辑”,因此是调用者处理该对象。

通过这种方式转移逻辑,调用者而不是被调用者运行该逻辑,因此,这发生在被调用者返回之前,在它清理其局部变量并使引用它的事物悬空之前。

这正是整个策略的要点!

现在,调用/使用上述函数时,不能再使用let绑定将函数的结果绑定到变量,因为该机制是为实际返回和调用者栈中实际运行的代码保留的。

相反,可以使用闭包/回调语法调用/使用with_hex函数

with_hex(66, |s| {
    println!("{}", s);
})

这非常强大,但每次创建这样的绑定都会产生一个向右漂移

with_hex(1, |one| {
    with_hex(2, |two| {
        with_hex(3, |three| {
            // ughhh ..
        })
    })
})

相反,如果编译器/语言提供一种方式,让let绑定神奇地执行这种转换,那就更好了

let one = hex(1);
let two = hex(2);
let three = hex(3);

以这种方式操作称为延续传递风格,在Rust中不能隐式完成。但这并不意味着不能为其提供糖。

出现了 #[with]

#[with] let one = hex(1);
#[with] let two = hex(2);
#[with] let three = hex(3);
  • 这也可以写成

    // in the scope of a `#[with('special)]`-annotated function.
    let one: &'special _ = hex(1);
    let two: &'special _ = hex(2);
    let three: &'special _ = hex(3);
    

    也就是说,具有“特殊生命周期”的 let 绑定。

当应用于函数时,它将所有这些注解的 let 绑定转换为嵌套闭包调用,其中绑定之后的(同一作用域内的)所有语句都移动到延续中。

以下是一个示例

# use ::with_locals::with; #[with('local)] fn hex (n: u32) -> &'local dyn ::core::fmt::Display { &format_args!("{:#x}", n) }
#
#[with]
fn hex_example ()
{
    let s: String = {
        println!("Hello, World!");
        #[with]
        let s_hex = hex(66);
        println!("s_hex = {}", s_hex); // Outputs `s_hex = 0x42`
        let s = s_hex.to_string();
        assert_eq!(s, "0x42");
        s
    };
    assert_eq!(s, "0x42");
}

上面的代码变为

# use ::with_locals::with; #[with('local)] fn hex (n: u32) -> &'local dyn ::core::fmt::Display { &format_args!("{:#x}", n) }
#
fn hex_example ()
{
    let s: String = {
        println!("Hello, World!");
        with_hex(66, |s_hex| {
            println!("s_hex = {}", s_hex); // Outputs `s_hex = 0x42`
            let s = s_hex.to_string();
            assert_eq!(s, "0x42");
            s
        })
    };
    assert_eq!(s, "0x42");
}

特质方法

特性也可以有 #[with] 注解的方法。

# use ::with_locals::with;
#
trait ToStr {
    #[with('local)]
    fn to_str (self: &'_ Self) -> &'local str
    ;
}

实现者的示例

# use ::with_locals::with; trait ToStr { #[with('local)] fn to_str (self: &'_ Self) -> &'local str ; }
#
impl ToStr for u32 {
    #[with('local)]
    fn to_str (self: &'_ u32) -> &'local str
    {
        let mut x = *self;
        if x == 0 {
            // By default, the macro tries to be quite smart and replaces
            // both implicitly returned and explicitly returned values, with
            // what the actual return of the actual `with_...` function must
            // be: `return f("0");`.
            return "0";
        }
        let mut buf = [b' '; 1 + 3 + 3 + 3]; // u32::MAX ~ 4_000_000_000
        let mut cursor = buf.len();
        while x > 0 {
            cursor -= 1;
            buf[cursor] = b'0' + (x % 10) as u8;
            x /= 10;
        }
        // return f(
        ::core::str::from_utf8(&buf[cursor ..]) // refers to a local!
            .unwrap()
        // );
    }
}
# #[with('special)]
# fn main ()
# {
#     let s: &'special str = 42.to_str();
#     assert_eq!(s, "42");
# }

特性的使用者示例(≠实现者)。

# use ::with_locals::with; trait ToStr { #[with('local)] fn to_str (self: &'_ Self) -> &'local str ; }
#
impl<T : ToStr> ::core::fmt::Display for __<T> {
    #[with('special)] // you can #[with]-annotate classic function,
                      // in order to get the `let` assignment magic :)
    fn fmt (self: &'_ Self, fmt: &'_ mut ::core::fmt::Formatter<'_>)
      -> ::core::fmt::Result
    {
        //      You can specify the
        //      special lifetime instead of applying `[with]`
        //      vvvvvvvv
        let s: &'special str = self.0.to_str();
        fmt.write_str(s)
    }
}
// (Using a newtype to avoid coherence issues)
struct __<T : ToStr>(T);

有关更详细的示例,请参阅可运行的文件中的 examples/main.rs

用法和“特殊生命周期”。

理解 #[with] 的工作方式非常重要的是,有时它必须执行转换(例如,将 foo() 调用转换为 with_foo(...) 调用),有时则不必;这取决于程序员想要编写的语义(也就是说,并非所有函数调用都依赖于CPS!)。

由于 过程宏仅在语法上操作,因此它无法理解这样的 语义(例如,如果 foo 不存在,则过程宏无法将其替换为 with_foo())。

因此,宏期望一些语法标记/提示,以便告诉它何时(以及在哪里!)进行操作。

  1. 显然,属性本身需要已经应用(在作用域函数上)。

    #[with('special)]
    fn ...
    
  2. 然后,宏将检查是否存在函数返回类型中的 “特殊生命周期”

    //        +-------------+
    //        |             |
    //     --------         V
    #[with('special)] // vvvvvvvv
    fn foo (...)   -> ...'special...
    

    这将触发将 fn foo 转换为 fn with_foo 的转换,并包含所有带回调参数的恶作剧。

    否则,它不会更改函数的原型。.

  3. 最后,宏还将检查函数体,以执行调用位置转换(例如,将 let x = foo(...) 转换为 with_foo(..., |x| { ... }))。

    这些转换仅应用于

    • #[with] 注解的语句: [with] let ...

    • 或者,应用于带有提及 “特殊生命周期” 的类型注解的语句。

      let x: ... 'special ... = foo(...);
      

备注

  • 默认情况下,“特殊生命周期”'ref。确实,由于 ref 是Rust的关键字,它不是一个合法的生命周期名称,因此它不可能与同名的一些真实生命周期参数发生冲突。

    编辑:对Rust和 rustc 的更新使得甚至宏也无法使用这样的生命周期名称。因此,'ref 和类似名称不再合法。

  • 但是 #[with] 允许您将生命周期重命名为您喜欢的名称,只需将其作为属性(当然是对函数应用的属性)的第一个参数提供即可

    use ::core::fmt::Display;
    use ::with_locals::with;
    
    #[with('local)]
    fn hello (name: &'_ str) -> &'local dyn Display
    {
        &format_args!("Hello {}!", name)
    }
    

高级用法

如果您对所有的这些CPS/回调样式都了如指掌,并且只想在定义基于回调的函数时有一些语法糖,但又不想属性打乱函数体内的代码(,如果您想禁用在 return 网点以及类似的魔法延续调用),

  • 例如,因为您正在与其他宏交互(由于它们导致 #[with] 无法“修复”内部的代码,这可能会导致无法编译的代码),

那么,请了解您可以

  • 直接使用手写的闭包调用 with_foo(...) 函数。

    鉴于函数的最终定义方式,这有点显而易见,并且绝对是一个不应被忽视的可能性。

  • 并且/或者,您可以将 continuation_name = some_identifier 参数添加到 #[with] 属性中,以禁用自动的 return continuation(<expr>) 转换;

    • 请注意,#[with] 将提供 some_identifier! 宏,该宏可以用作 return some_identifier(...) 的缩写。

      例如,如果标识符为 return_,则可以编写 return_!(value),这可以正确展开为 return return_(value)(返回延续返回的值)。

示例

use ::core::fmt::Display;
use ::with_locals::with;

#[with('local, continuation_name = return_)]
fn display_addr (addr: usize) -> &'local dyn Display
{
    if addr == 0 {
        return_!( &"NULL" );
    }
    with_hex(addr, |hex| {
        return_(&format_args!("0x{}", hex))
    })
}
// where
#[with('local)]
fn hex (n: usize) -> &'local dyn Display
{
    &format_args!("{:x}", n)
}

强大的去糖化

由于一些语句被封装在闭包中,因此仅此转换就会使控制流语句(如 return?break)在 #[with] 语句的作用域内停止工作(之后)。

use ::core::fmt::Display;
use ::with_locals::with;

#[with('local)]
fn hex (n: u32) -> &'local dyn Display
{
    &format_args!("{:#x}", n)
}

fn main ()
{
    for n in 0 .. { // <- `break` cannot refer to this:
        with_hex(n, |s| { // === closure boundary ===
            println!("{}", s);     // ^ Error!
            if n >= 5 {            // |
                break; // ------------+
            }
        })
    }
}

然而,当使用 #[with] 语法糖时,上述模式似乎可以正常工作

use ::core::fmt::Display;
use ::with_locals::with;

#[with('local)]
fn hex (n: u32) -> &'local dyn Display
{
    &format_args!("{:#x}", n)
}

#[with]
fn main ()
{
    for n in 0 .. {
        #[with]
        let s = hex(n);
        println!("{}", s);
        if n >= 5 {
            break;
        }
    };
}
  • 点击此处查看如何实现

    这是通过在提供的闭包的返回值中捆绑预期的控制流信息来实现的

    for n in 0 .. {
        enum ControlFlow<T> {
            /// The statements evaluated to a value of type `T`.
            Eval(T),
    
            /// The statements "called" `break`.
            Break,
        }
    
        match with_hex(n, |s| ControlFlow::Eval({
            println!("{}", s);
            if n >= 5 {
                return ControlFlow::Break;
            }
        }))
        {
            ControlFlow::Eval(it) => it,
            ControlFlow::Break => break,
        }
    }
    

调试/宏展开

如果您出于某种原因想要查看由#[with]属性调用生成的实际代码,那么您只需启用expand-macros Cargo功能即可

[dependencies]
# ...
with_locals = { version = "...", features = ["expand-macros"] }

这将显示与cargo-expand非常相似的输出代码风格,但有两个额外的优势

  • 它不会展开所有宏,只会展开#[with]。因此,如果在函数体内部有一个类似于println!的调用,实际的内部格式化逻辑/机制将保持隐藏,不会破坏代码。

  • 一旦启用Cargo功能,可以使用一个特殊的环境变量来过滤所需的展开

    WITH_LOCALS_DEBUG_FILTER=pattern cargo check
    
    • 这将只显示名称包含给定模式的函数的展开。请注意,这并不涉及完全限定名称(包括外部模块),只涉及裸名称。
  • 话虽如此,这仅在过程宏被评估时有效,而rustc将尝试缓存此类调用的结果。如果是这种情况,您只需在相关文件中执行一些虚拟更改,并保存

依赖关系

~1.5MB
~36K SLoC