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日 |
2988 在 Rust模式
28,268 每月下载量
在 2 个crate中使用(通过 with_locals)
68KB
1.5K SLoC
::with_locals
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()
))。
因此,宏期望一些语法标记/提示,告诉它何时(以及在哪里!)工作
-
显然,属性本身需要已经应用(在封装函数上)
#[with('special)] fn ...
-
然后,宏将检查函数返回类型中是否存在 “特殊生命周期”。
// +-------------+ // | | // -------- V #[with('special)] // vvvvvvvv fn foo (...) -> ...'special...
这将触发将
fn foo
转换为fn with_foo
,以及所有携带回调参数的恶作剧。否则,它不会改变函数的原型.
-
最后,宏还将检查函数体,以执行调用点转换(例如,将
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
、?
、continue
和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