Това вече сме го виждали
macro_rules! try {
($expr:expr) => {
match $expr {
Ok(value) => value,
Err(e) => return Err(e.into()),
}
}
}
Общата схема
macro_rules! add {
($var1:expr, $var2:expr) => {
$var1 + $var2;
}
}
fn main() {
println!("{}", add!(1, 1));
println!("{}", add!("foo".to_string(), "bar"));
}
Общата схема
macro_rules!
всъщност не е макро, а е "syntax extension", имплементирано на ниво компилатор
(...) => { ... }
$var1
, $var2
са от "тип" expression — цялостен изразЗащо не "променливи"? Защото в кръглите скоби се прави pattern-matching на ниво tokens:
macro_rules! add {
(Чш, я събери ($var1:expr) и ($var2:expr)) => {
$var1 + $var2;
}
}
fn main() {
println!("{}", add!(Чш, я събери (1) и (1)));
println!("{}", add!(Чш, я събери ("foo".to_string()) и ("bar")));
}
Защо има скоби? За да се знае къде свършва expression/израз.
macro_rules! add {
(Чш, я събери $var1:expr и $var2:expr) => {
$var1 + $var2;
}
}
error: `$var1:expr` is followed by `и`, which is not allowed for `expr` fragments --> src/main.rs:2:30 | 2 | (Чш, я събери $var1:expr и $var2:expr) => { | ^
За expr
са позволени само ,
, ;
и =>
, ако не са в скоби
macro_rules! add {
(Чш, я събери $var1:expr, $var2:expr) => {
$var1 + $var2;
}
}
fn main() {
println!("{}", add!(Чш, я събери 1, 1));
println!("{}", add!(Чш, я събери "foo".to_string(), "bar"));
}
Нещо малко по-практично
macro_rules! map {
{
$( $key: expr : $value: expr ),*
} => {
// Забележете блока
{
let mut map = ::std::collections::HashMap::new();
$( map.insert($key, $value); )*
map
}
}
}
Какво прави $( ... ),*
?
$( ... )
и едно от двете:
*
- 0 или повече повторения
+
- 1 или повече повторения
,
$( ... ),*
търси нещо от вида ... , ... , ...
Ок, нека да компилираме
error: `$key:expr` is followed by `:`, which is not allowed for `expr` fragments --> src/main.rs:4:23 | 4 | $( $key: expr : $value: expr ),* | ^
Правилата са си правила.. Ще ги разгледаме подробно по-късно
macro_rules! map {
{
$( $key: expr => $value: expr ),*
} => {
{
let mut map = ::std::collections::HashMap::new();
$( map.insert($key, $value); )*
map
}
}
}
let m = map! {
"a" => 1,
"b" => 2
};
println!("{:?}", m);
Изход:
{"b": 2, "a": 1}
А какво става, ако искаме да поддържаме trailing comma 🤔
let m = map! {
"a" => 1,
"b" => 2,
};
println!("{:?}", m);
error: unexpected end of macro invocation --> src/main.rs:4:17 | 4 | "b" => 2, | ^
Не точно каквото очаквахме..
Може би така?
macro_rules! map {
{
$( $key: expr => $value: expr ),*,
} => {
/* ... */
}
}
let m = map! {
"a" => 1,
"b" => 2
};
error: unexpected end of macro invocation --> src/main.rs:4:16 | 4 | "b" => 2 | ^
Не..
Не бойте се, има си трик за това
macro_rules! map {
{
$( $key: expr => $value: expr ),* $(,)*
} => {
/* ... */
}
}
Недостатъка е, че може да match-нем нещо такова. Ще покажем по-късно и друг начин
let m = map! {
"a" => 1,
"b" => 2,,,,,,,,,,,,
};
Guess macro_rules! ¯\_(ツ)_/¯
В момента макросите са в процес на преработка към macros 2.0, където ще има ?
Макросите в Rust са хигиенични
macro_rules! five_times {
($x:expr) => (5 * $x);
}
assert_eq!(25, five_times!(2 + 3));
Нещо подобно в C/C++ би изчисло 13
В този пример отново заради хигиена двата state-а не се shadow-ват взаимно
macro_rules! log {
($msg:expr) => {{
let state: i32 = get_log_state();
if state > 0 {
println!("log({}): {}", state, $msg);
}
}};
}
let state: &str = "reticulating splines";
log!(state);
Всяко разгъване на макрос се случва в различен синтактичен контекст. В този случай може да го мислите все едно двете променливи имат различен цвят който ги разграничава.
По тази причина не може да представяме нови променливи чрез макрос по следния начин
macro_rules! foo {
() => (let x = 3;);
}
foo!();
println!("{}", x); // компилационна грешка
Ще трябва да подадем името на променлива на макроса за да се получи
macro_rules! foo {
($v:ident) => (let $v = 3;);
}
foo!(x);
println!("{}", x);
Правило важи за let
и цикли като loop while for
,
но не и за items
, което значи, че следното ще се компилира
macro_rules! foo {
() => (fn x() { });
}
foo!();
x();
Макросите следват същите правила както останалата част от синтаксиса на Rust
foo!(...);
foo![...];
foo! { ... }
foo!([)
е невалидно
Формално извикването на макрос се състои от поредица от token trees
които са
()
, []
или {}
Затова Rust макросите винаги приоритизират затварянето на скобите пред match-ването, което е полезно при някои подходи за match-ване
Tиповете на метапроменливите са
ident
: an identifier. x
; foo
path
: a qualified name. T::SpecialA
expr
: an expression. 2 + 2
; if true { 1 } else { 2 }
; f(42)
ty
: a type. i32
; Vec<(char, String)>
; &T
pat
: a pattern. Some(t)
; (17, 'a')
; _
stmt
: a single statement. let x = 3
block
: a brace-delimited sequence of statements and optionally an expression. { log(error, "hi"); return 12; }
item
: an item. fn foo() { }
; struct Bar;
meta
: a "meta item", as found in attributes. cfg(target_os = "windows")
tt
: a single token tree.Ограниченията за типовете са
expr
and stmt
variables may only be followed by one of: => , ;
ty
and path
variables may only be followed by one of: => , = | ; : > [ { as where
pat
variables may only be followed by one of: => , = | if in
Макросите могат да имат повече от един ръкав за matching разделени с ;
macro_rules! my_macro {
($e: expr) => (...);
($i: ident) => (...);
(for $i: ident in $e: expr) => (...);
}
Има и конвенция за private ръкави @text
, които да се викат чрез рекурсия
macro_rules! my_macro {
(for $i: ident in $e: expr) => (...);
(@private1 $e: expr) => (...);
(@private2 $i: ident) => (...);
}
Макросите могат да извикват други макроси и дори себе си както този прост html shorthand
macro_rules! write_html {
($w: expr, ) => (());
($w: expr, $e: tt) => (write!($w, "{}", $e));
($w: expr, $tag: ident [ $( $inner: tt )* ] $( $rest: tt )*) => {{
write!($w, "<{}>", stringify!($tag));
write_html!($w, $($inner)*);
write!($w, "</{}>", stringify!($tag));
write_html!($w, $($rest)*);
}};
}
fn main() {
use std::fmt::Write;
let mut out = String::new();
write_html! {
&mut out,
html[
head[title["Macros guide"]]
body[h1["Macros are the best!"]]
]
}
assert_eq!(out,
"<html><head><title>Macros guide</title></head>\
<body><h1>Macros are the best!</h1></body></html>");
}
Нека направим онзи хак за trailing comma по-малко хак с тези познания
macro_rules! map {
{ $( $key: expr => $value: expr ),*, } => {
map!( $( $key => $value ),* );
};
{ $( $key: expr => $value: expr ),* } => {
{
let mut map = ::std::collections::HashMap::new();
$( map.insert($key, $value); )*
map
}
};
}
Компилатора разгъва макросите в ранна фаза на компилация, затова имат специфична видимост
#[macro_use]
преди мястото, където го ползватеИмаме макроси дефинирани в macros
и ще ги позлваме в client
#[macro_use]
mod macros;
mod client; // ок
mod client; // компилационна грешка
#[macro_use]
mod macros;
Макроси дефинирани в блокове, функции или други подобни конструкции са видими само там
fn main() {
macro_rules! map { ... }
}
При работа на ниво crate
#[macro_use]
за импортиране на всичко или #[macro_use(my_macro, other_macro)]
#[macro_export]
Дебъгването на макроси е сложно, но има някои полезни команди
rustc --pretty expanded
--pretty expanded,hygiene
за да се запазят syntax scope-овете
cargo +nightly rustc -- -Z unstable-options --pretty=expanded
Има и удобни, но нестабилни макроси, които се ползват през feature gate на nightly
log_syntax!(...)
- принтира аргументите си при компилация на stdout и се разгръща до нищо
trace_macros!(true)
- включва компилаторни съобщения при разгръщане на макрос
trace_macros!(false)
- изключва съобщениятаpanic!
- панира програмата
vec!
- създава вектор от елементи
assert!
& assert_eq!
- използват се при тестове за проверка на данните
macro_rules! write_html {
($w: expr, ) => (());
($w: expr, $e: tt) => (write!($w, "{}", $e));
($w: expr, $tag: ident [ $( $inner: tt )* ] $( $rest: tt )*) => {{
write!($w, "<{}>", stringify!($tag));
write_html!($w, $($inner)*);
write!($w, "</{}>", stringify!($tag));
write_html!($w, $($rest)*);
}};
}
write_html! {
&mut out,
html[
head[title["Macros guide"]]
body[h1["Macros are the best!"]]
]
}
Макрос, който инизиализира масив до 3 елемента
macro_rules! init_array {
[$e:expr; $n:tt] => {{
let e = $e;
init_array!(@accum ($n, e.clone()) -> ())
}};
(@accum (3, $e:expr) -> ($($body:tt)*)) => { init_array!(@accum (2, $e) -> ($($body)* $e,)) };
(@accum (2, $e:expr) -> ($($body:tt)*)) => { init_array!(@accum (1, $e) -> ($($body)* $e,)) };
(@accum (1, $e:expr) -> ($($body:tt)*)) => { init_array!(@accum (0, $e) -> ($($body)* $e,)) };
(@accum (0, $_e:expr) -> ($($body:tt)*)) => { init_array!(@as_expr [$($body)*]) };
(@as_expr $e:expr) => { $e };
}
let strings: [String; 3] = init_array![String::from("hi!"); 3];
А не може ли да опростим нещата до това?
macro_rules! init_array {
(@accum 0, $_e:expr) => {/* empty */};
(@accum 1, $e:expr) => {$e};
(@accum 2, $e:expr) => {$e, init_array!(@accum 1, $e)};
(@accum 3, $e:expr) => {$e, init_array!(@accum 2, $e)};
[$e:expr; $n:tt] => {
{
let e = $e;
[ init_array!(@accum $n, e) ]
}
};
}
Не, защото това би довело до следното разгъване
init_array!(@accum 3, e)
e, init_array!(@accum 2, e)
e, e, init_array!(@accum 1, e)
e, e, e
[e, e, e]
Тук всяка помощна стъпка ще е невалиден Rust синтаксис и това не е позволено независимо от стъпките
Push-Down ни позволява да правим подобни констрикции чрез акумулиране на токени, без да се налага да имаме валиден синтаксис през цялото време.
Разгъвка на първия пример изглежда така
init_array! { String:: from ( "hi!" ) ; 3 }
init_array! { @ accum ( 3 , e . clone ( ) ) -> ( ) }
init_array! { @ accum ( 2 , e.clone() ) -> ( e.clone() , ) }
init_array! { @ accum ( 1 , e.clone() ) -> ( e.clone() , e.clone() , ) }
init_array! { @ accum ( 0 , e.clone() ) -> ( e.clone() , e.clone() , e.clone() , ) }
init_array! { @ as_expr [ e.clone() , e.clone() , e.clone() , ] }
Push-Down Accumulation се използва в комбинация с TT Muncher, за да се парсват произволно сложни граматики