16个版本
0.3.3 | 2024年8月10日 |
---|---|
0.3.2 | 2023年7月3日 |
0.3.1 | 2023年3月24日 |
0.3.0 |
|
0.0.1-alpha-2 | 2020年9月1日 |
213 in Rust模式
28,256 monthly downloads
用于 stackbox
28KB
221 行
::with_locals
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()
)。
因此,宏期望一些语法标记/提示,以便告诉它何时(以及在哪里!)进行操作。
-
显然,属性本身需要已经应用(在作用域函数上)。
#[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
网点以及类似的魔法延续调用),
- 例如,因为您正在与其他宏交互(由于它们导致
#[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