15个版本

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.1.1 2020年10月5日

2988Rust模式

Download history 12045/week @ 2024-04-20 16251/week @ 2024-04-27 11286/week @ 2024-05-04 14727/week @ 2024-05-11 15726/week @ 2024-05-18 16064/week @ 2024-05-25 17121/week @ 2024-06-01 11369/week @ 2024-06-08 13966/week @ 2024-06-15 17244/week @ 2024-06-22 13227/week @ 2024-06-29 7497/week @ 2024-07-06 7015/week @ 2024-07-13 7867/week @ 2024-07-20 6934/week @ 2024-07-27 5267/week @ 2024-08-03

28,268 每月下载量
2 个crate中使用(通过 with_locals

Zlib OR MIT OR Apache-2.0

68KB
1.5K SLoC

::with_locals

Repository Latest version Documentation MSRV unsafe forbidden License CI

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

让我们从一个基本示例开始:返回 / 产生一个 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 不存在,则过程宏无法将 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 位置 & co. 跳过魔法 continuation 调用),

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

那么,请了解您可以

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

    鉴于函数的定义方式,这很显然,绝对是一个不应该被忽视的可能性。

  • 您还可以向 #[with] 属性添加一个 continuation_name = some_identifier 参数来禁用自动的 return continuation(<expr>) 转换;

    • 注意,此时 #[with] 将提供一个 some_identifier! 宏,它可以作为 return some_identifier(...) 的简称。

      如果使用的标识符是,例如,return_,这尤其方便:您可以在需要经典函数返回值的地方写 return_!( value ),而经典函数将写 return value,并且它将正确地展开为 return return_(value)(返回 continuation 返回的值)。

示例

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?continuebreak在位于#[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