云服务器价格_云数据库_云主机【优惠】最新活动-搜集站云资讯

网站服务器_mysql数据库导入sql文件_返利

小七 141 0

用Rust编写复杂宏:反向抛光符号

(这是我个人博客上最初发表的一篇教程的交叉贴)在其他有趣的特性中,Rust有一个强大的宏系统。不幸的是,即使在阅读了这本书和各种教程之后,当我试图实现一个涉及处理不同元素的复杂列表的宏时,我仍然很难理解该如何实现它,并且花了一些时间,直到我到达"叮"的那一刻,我开始把宏错误地用于所有事情:)(好吧,不是所有的东西)就像我在使用宏,因为我不想使用函数和指定类型和生命周期,就像我见过的一些人做的那样,但是在任何地方它实际上是有用的)Conor Lawless的CC BY 2.0图像所以,下面是我对描述编写这些宏背后的原则的看法。它假设您已经阅读了本书中的宏部分,并且熟悉基本的宏定义和标记类型。在本教程中,我将以反向波兰符号为例。它很有趣,因为它非常简单,您可能在学校就已经熟悉它了,但是要在编译时静态地实现它,您已经需要使用递归宏方法了。反向波兰表示法(也称为后缀表示法)对其所有操作使用堆栈,以便将任何操作数推送到堆栈上,任何[二进制]运算符都从堆栈中获取两个操作数,计算结果并放回原处。所以下面的表达式:2 3+4*转化为:把2放在栈上。把3放在栈上。从堆栈中取最后两个值(3和2),应用运算符+并将结果(5)放回堆栈。放在第四层。从堆栈中取最后两个值(4和5),应用运算符*(4*5),然后将结果(20)放回堆栈。表达式结束时,堆栈上的单个值就是结果(20)。在数学和大多数现代编程语言中使用的更常见的中缀表示法,表达式看起来像(2+3)*4。因此,让我们编写一个宏,通过将RPN转换成Rust能够理解的中缀符号,在编译时计算RPN。宏规则!rpn公司{//托多}println!("{}",rpn!(2 3+4*);//20让我们从把数字压入堆栈开始。宏目前不允许匹配字面值,expr不适合我们,因为它可能会意外地匹配序列,比如2+3。。。而不是只取一个数字,所以我们将求助于tt—一个只匹配一个令牌树的通用令牌匹配器(无论是像literal/identifier/lifetime/etc.这样的原始标记,还是包含更多标记的()/[]/{}-括号表达式):宏规则!rpn公司{($数字:tt) => {//托多};}现在,我们需要一个堆栈变量。宏不能使用实变量,因为我们希望这个堆栈只在编译时存在。因此,诀窍是有一个单独的令牌序列,它可以被传递,并作为累加器使用。在我们的例子中,让我们将它表示为一个逗号分隔的expr序列(因为我们不仅将使用它来处理简单的数字,而且还将其用于中间中缀表达式),并将其包装到括号中,以与其他输入分隔开:宏规则!rpn公司{([ $($堆栈:表达式),* ] $数字:tt) => {//托多};}现在,令牌序列并不是一个真正的变量—您不能在适当的地方修改它,然后再做一些事情。相反,您可以创建此标记序列的新副本并进行必要的修改,然后再次递归调用同一个宏。如果您来自函数式语言背景,或者曾经在任何提供不可变数据的库中工作过,那么这两种方法(通过创建修改后的副本来改变数据)和使用递归处理列表-可能已经很熟悉了:宏规则!rpn公司{([ $($堆栈:表达式),* ] $数字:tt) => {rpn!([$num$(,$stack)*])};}显然,只有一个数字的情况是不太可能的,我们也不太感兴趣,因此我们需要将该数字之后的任何其他内容作为零个或多个tt标记的序列进行匹配,这些标记可以传递给下一次调用我们的宏以进行进一步的匹配和处理:宏规则!rpn公司{([ $($堆栈:表达式),* ] $数字:tt$($休息时间:tt)*) => {rpn!([$num$(,$stack)*]$($rest)*)};}目前我们仍然缺少运营商的支持。如何匹配运算符?如果我们的RPN是一个令牌序列,我们希望以完全相同的方式处理,我们可以简单地使用一个列表,比如$($令牌:tt)*. 不幸的是,这并不能使我们能够遍历list并根据每个标记推送操作数或应用运算符。这本书说"宏系统根本不处理解析歧义",这对于一个宏分支来说是正确的——我们不能匹配后面跟运算符的序列$($数字:tt)*+因为+也是一个有效的标记,并且可以被tt组匹配,但是递归宏在这方面也有帮助。如果宏定义中有不同的分支,Rust会逐个尝试,因此我们可以将运算符分支放在数值分支之前,这样可以避免任何冲突:宏规则!rpn公司{([ $($堆栈:表达式),* ] + $($休息时间:tt)*) => {//托多};([ $($堆栈:表达式),* ] - $($休息时间:tt)*) => {//托多};([ $($堆栈:表达式),* ] * $($休息时间:tt)*) => {//托多};([ $($堆栈:表达式),* ] / $($休息时间:tt)*) => {//托多};([ $($堆栈:表达式),* ] $数字:tt$($休息时间:tt)*) => {rpn!([$num$(,$stack)*]$($rest)*)};}如前所述,运算符应用于堆栈上的最后两个数字,因此我们需要分别匹配它们,"计算"结果(构造一个正则中缀表达式)并将其放回:宏规则!rpn公司{([$b:expr,$a:expr$($堆栈:表达式)* ] + $($休息时间:tt)*) => {rpn!([$a+$b$(,$stack)*]$($rest)*)};([$b:expr,$a:expr$($堆栈:表达式)* ] - $($休息时间:tt)*) => {rpn!([$a-$b$(,$stack)*]$($rest)*)};([$b:expr,$a:expr$($堆栈:表达式)* ] * $($休息时间:tt)*) => {rpn!([$a*$b$(,$stack)*]$($rest)*)};([$b:expr,$a:expr$($堆栈:表达式)* ] / $($休息时间:tt)*) => {rpn!([$a/$b$(,$stack)*]$($rest)*)};([ $($堆栈:表达式),* ] $数字:tt$($休息时间:tt)*) => {rpn!([$num$(,$stack)*]$($rest)*)};}我不太喜欢这种明显的重复,但是,就像文字一样,没有特殊的标记类型来匹配运算符。但是,我们可以做的是添加一个负责求值的助手,并将任何显式运算符分支委托给它。在宏中,不能真正使用外部助手,但唯一可以确定的是,宏已经在作用域中,因此通常的技巧是在同一宏中用某个唯一的标记序列"标记"一个分支,然后像在常规分支中那样递归地调用它。让我们使用@op作为这样的标记,并通过它内部的tt接受任何运算符(tt在这种上下文中是明确的,因为我们将只向这个helper传递运算符)。而且堆栈不再需要在每个单独的分支中展开—因为我们之前将它包装到[]括号中,所以它可以作为任何另一个令牌树(tt)进行匹配,然后传递给我们的助手:宏规则!rpn公司{(@op[$b:expr,$a:expr$($堆栈:表达式)*]$op:tt$($休息时间:tt)*) => {rpn!([$a$op$b$(,$stack)*]$($rest)*)};($堆栈:tt+$($休息时间:tt)*) => {rpn!(@op$stack+$($rest)*)};($堆栈:tt-$($休息时间:tt)*) => {rpn!(@op$stack-$($rest)*)};($堆栈:tt* $($休息时间:tt)*) => {rpn!(@op$stack*$($rest)*)};($堆栈:tt/$($休息时间:tt)*) => {rpn!(@op$stack/$($rest)*)};([ $($堆栈:表达式),* ] $数字:tt$($休息时间:tt)*) => {rpn!([$num$(,$stack)*]$($rest)*)};}现在所有的令牌都是由相应的分支来处理的,我们只需要在堆栈中包含一个项目时处理最后的情况,并且不再剩下令牌:宏规则!rpn公司{// ...([ $结果:expr]) => {$结果};}此时,如果使用空堆栈和RPN表达式调用此宏,则它将已经生成正确的结果:游乐场println!("{}",rpn!([]23+4*);//20但是,我们的堆栈是一个实现细节,我们真的不希望每个使用者都传入一个空堆栈,所以让我们在最后添加另一个catch all分支,它将作为入口点并自动添加[]:游乐场宏规则!rpn公司{// ...($($代币:tt)*) => {rpn!([]$($代币)*)};}println!("{}",rpn!(2 3+4*);//20我们的宏甚至适用于更复杂的表达式,比如维基百科关于RPN的页面!println!("{}",rpn!(15 7 1 1+-/3*2 1 1++-);//5错误处理现在,对于正确的RPN表达式,一切似乎都很顺利,但要使宏准备生产,我们需要确保它也能处理无效的输入,并给出合理的错误消息。首先,让我们尝试在中间插入另一个数字,看看会发生什么:println!("{}",rpn!(2 3 7+4*);输出:错误[E0277]:未满足特征绑定`[{integer};2]:std::fmt::Display`不满足-->src公司/主。rs:36:20个|36 |打印!("{}",rpn!(2+7);|^^^^^^^^^^^^^^^^^^^`[{integer};2]`无法使用默认格式设置工具格式化;请尝试使用`:?`如果使用的是格式字符串|=help:没有为`[{integer};2]实现trait`std::fmt::Display``=注意:`std::fmt::Display::fmt必需`好吧,这看起来毫无帮助,因为它没有提供任何与表达式中实际错误相关的信息。为了弄清楚发生了什么,我们需要调试宏。为此,我们将使用trace_macros特性(而且,与其他可选编译器功能一样,您将需要Rust的夜间版本)。我们不想追踪println!调用,这样我们将把RPN计算分离为一个变量:游乐场#![功能(跟踪宏)]宏规则!rpn{/*。。。*/ }fn主(){追踪宏!(正确);设e=rpn!(2 3 7+4*);追踪宏!(错误);println!("{}",e);}在输出中,我们将逐步了解宏是如何递归求值的:注:trace U宏-->src公司/主要:39卢比:13个|39 |让e=rpn!(2 3 7+4*);| ^^^^^^^^^^^^^^^^^|=注:exp