19. Макроси

19. Макроси

19. Макроси

15 декември 2017

Преговор

Макроси

Макроси

try!

Това вече сме го виждали

macro_rules! try {
    ($expr:expr) => {
        match $expr {
            Ok(value) => value,
            Err(e) => return Err(e.into()),
        }
    }
}

add!

Общата схема

macro_rules! add {
    ($var1:expr, $var2:expr) => {
        $var1 + $var2;
    }
}

fn main() {
    println!("{}", add!(1, 1));
    println!("{}", add!("foo".to_string(), "bar"));
}

add!

Общата схема

add!

Защо не "променливи"? Защото в кръглите скоби се прави pattern-matching на ниво tokens:

macro_rules! add {
    (Чш, я събери ($var1:expr) и ($var2:expr)) => {
        $var1 + $var2;
    }
}

fn main() {
    println!("{}", add!(Чш, я събери (1) и (1)));
    println!("{}", add!(Чш, я събери ("foo".to_string()) и ("bar")));
}

add!

Защо има скоби? За да се знае къде свършва 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) => {
  |                              ^

add!

За expr са позволени само ,, ; и =>, ако не са в скоби

macro_rules! add {
    (Чш, я събери $var1:expr, $var2:expr) => {
        $var1 + $var2;
    }
}

fn main() {
    println!("{}", add!(Чш, я събери 1, 1));
    println!("{}", add!(Чш, я събери "foo".to_string(), "bar"));
}

map!

Нещо малко по-практично

macro_rules! map {
  {
    $( $key: expr : $value: expr ),*
  } => {
    // Забележете блока
    {
      let mut map = ::std::collections::HashMap::new();
      $( map.insert($key, $value); )*
      map
    }
  }
}

map!

Какво прави $( ... ),*?

map!

Ок, нека да компилираме

map!

error: `$key:expr` is followed by `:`, which is not allowed for `expr` fragments
--> src/main.rs:4:23
  |
4 |         $( $key: expr : $value: expr ),*
  |                       ^

map!

Правилата са си правила.. Ще ги разгледаме подробно по-късно

macro_rules! map {
  {
    $( $key: expr => $value: expr ),*
  } => {
    {
      let mut map = ::std::collections::HashMap::new();
      $( map.insert($key, $value); )*
      map
    }
  }
}

map!

let m = map! {
    "a" => 1,
    "b" => 2
};

println!("{:?}", m);

Изход:

{"b": 2, "a": 1}

map!

А какво става, ако искаме да поддържаме trailing comma 🤔

let m = map! {
    "a" => 1,
    "b" => 2,
};

println!("{:?}", m);

map!

error: unexpected end of macro invocation
--> src/main.rs:4:17
  |
4 |         "b" => 2,
  |                 ^

map!

Не точно каквото очаквахме..

map!

Може би така?

macro_rules! map {
  {
    $( $key: expr => $value: expr ),*,
  } => {
    /* ... */
  }
}

map!

let m = map! {
    "a" => 1,
    "b" => 2
};
error: unexpected end of macro invocation
--> src/main.rs:4:16
  |
4 |         "b" => 2
  |                ^

map!

Не..

map!

Не бойте се, има си трик за това

macro_rules! map {
  {
    $( $key: expr => $value: expr ),* $(,)*
  } => {
    /* ... */
  }
}

map!

Недостатъка е, че може да match-нем нещо такова. Ще покажем по-късно и друг начин

let m = map! {
    "a" => 1,
    "b" => 2,,,,,,,,,,,,
};

map!

Guess macro_rules! ¯\_(ツ)_/¯

macros 2.0

В момента макросите са в процес на преработка към 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

Синтаксис

Синтаксис

Формално извикването на макрос се състои от поредица от token trees които са

Синтаксис

Затова Rust макросите винаги приоритизират затварянето на скобите пред match-ването, което е полезно при някои подходи за match-ване

Синтаксис

Metavariables & Fragment specifiers

Tиповете на метапроменливите са

Синтаксис

Metavariables & Fragment specifiers

Ограниченията за типовете са

Ръкави

Макросите могат да имат повече от един ръкав за 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
    }
  };
}

Scoping

Компилатора разгъва макросите в ранна фаза на компилация, затова имат специфична видимост

Scoping

Имаме макроси дефинирани в macros и ще ги позлваме в client

#[macro_use]
mod macros;
mod client; // ок
mod client; // компилационна грешка
#[macro_use]
mod macros;

Scoping

Макроси дефинирани в блокове, функции или други подобни конструкции са видими само там

fn main() {
    macro_rules! map { ... }
}

Scoping

При работа на ниво crate

Debugging

Дебъгването на макроси е сложно, но има някои полезни команди

Debugging

Има и удобни, но нестабилни макроси, които се ползват през feature gate на nightly

Стандартни макроси

Advanced

TT Muncher

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)*);
    }};
}

Advanced

TT Muncher

write_html! {
    &mut out,
    html[
        head[title["Macros guide"]]
        body[h1["Macros are the best!"]]
    ]
}

Advanced

Push-Down Accumulation

Макрос, който инизиализира масив до 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];

Advanced

Push-Down Accumulation

А не може ли да опростим нещата до това?

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) ]
        }
    };
}

Advanced

Push-Down Accumulation

Не, защото това би довело до следното разгъване

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 синтаксис и това не е позволено независимо от стъпките

Advanced

Push-Down Accumulation

Push-Down ни позволява да правим подобни констрикции чрез акумулиране на токени, без да се налага да имаме валиден синтаксис през цялото време.

Advanced

Push-Down Accumulation

Разгъвка на първия пример изглежда така

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() , ] }

Advanced

Push-Down Accumulation се използва в комбинация с TT Muncher, за да се парсват произволно сложни граматики

Материали

Въпроси