撰寫巨集

撰寫巨集可以到非常複雜。而你幾乎永遠都不需要寫巨集,但有時你可能會因為它們非常方便而想去寫。寫巨集很有趣,因為它們幾乎是不同的語言。寫巨集時你實際上會用到另一個叫 macro_rules! 的巨集。然後加入你的巨集名稱,並開啟 {} 區塊。裡面有點像 match 陳述式。

這裡有個巨集的範例只有接受 (),也只回傳 6:

macro_rules! give_six {
    () => {
        6
    };
}

fn main() {
    let six = give_six!();
    println!("{}", six);
}

但它和 match 陳述式不太一樣,因為巨集實際上不會編譯任何東西。它只是接受一個輸入並給出一個輸出。然後編譯器會檢查它是否合理。這就是為什麼巨集就像是"寫程式碼的程式碼"。你會記得,真正的 match 陳述式需要給出相同的型別,所以這個就會不能編譯:

fn main() {
// ⚠️
    let my_number = 10;
    match my_number {
        10 => println!("You got a ten"),
        _ => 10,
    }
}

它會抱怨你在一種情況下要回傳 (),卻在另一種情況下要回傳 i32

error[E0308]: `match` arms have incompatible types
 --> src\main.rs:5:14
  |
3 | /     match my_number {
4 | |         10 => println!("You got a ten"),
  | |               ------------------------- this is found to be of type `()`
5 | |         _ => 10,
  | |              ^^ expected `()`, found integer
6 | |     }
  | |_____- `match` arms have incompatible types

但巨集並不關心,因為它只是給予輸出。它不是編譯器——它是程式碼的程式碼。所以你可以這樣做:

macro_rules! six_or_print {
    (6) => {
        6
    };
    () => {
        println!("You didn't give me 6.");
    };
}

fn main() {
    let my_number = six_or_print!(6);
    six_or_print!();
}

這就好辦了,印出 You didn't give me 6.。你也可以看到,這不是匹配陳述式的分支,因為沒有 _ 的情況。我們只能給它 (6),或者 (),其他的都會出錯。而我們給它的 6 甚至不是 i32,只是輸入的 6。其實你可以設定任何東西作為巨集的輸入,因為它只查看輸入見到了什麼。比如說:

macro_rules! might_print {
    (THis is strange input 하하はは哈哈 but it still works) => {
        println!("You guessed the secret message!")
    };
    () => {
        println!("You didn't guess it");
    };
}

fn main() {
    might_print!(THis is strange input 하하はは哈哈 but it still works);
    might_print!();
}

所以這個奇怪的巨集只回應兩件事。()(THis is strange input 하하はは哈哈 but it still works)。沒有其他的東西。印出:

You guessed the secret message!
You didn't guess it

所以巨集不完全是 Rust 語法。但是巨集也可以理解你給它的不同型別的輸入。拿這個例子來說:

macro_rules! might_print {
    ($input:expr) => {
        println!("You gave me: {}", $input);
    }
}

fn main() {
    might_print!(6);
}

會印出 You gave me: 6$input:expr 的部分很重要。它的意思是"對於表達式,給它取變數名稱為 $input"。巨集中的變數是以 $ 開頭。在這個巨集中,如果你給它表達式,表達式就會印出來。讓我們再來多試幾次:

macro_rules! might_print {
    ($input:expr) => {
        println!("You gave me: {:?}", $input); // 現在我們將會使用 {:?} 因為我們將會給它不同的種類的表達式
    }
}

fn main() {
    might_print!(()); // 給它 ()
    might_print!(6); // 給它 6
    might_print!(vec![8, 9, 7, 10]); // 給它向量
}

會印出:

You gave me: ()
You gave me: 6
You gave me: [8, 9, 7, 10]

另外注意,我們寫的是 {:?},但它不會檢查 &input 是否實現了 Debug。它只會寫程式碼,並嘗試讓它編譯,如果不行,那它就會給出錯誤。

那麼除了 expr fragment,巨集還能看到什麼呢?它們是 block | expr | ident | item | lifetime | literal | meta | pat | path | stmt | tt | ty | vis。這就是複雜的部分。你可以在這裡看到它們各自的意思,這裡說:

item: an Item
block: a BlockExpression
stmt: a Statement without the trailing semicolon (except for item statements that require semicolons)
pat: a Pattern
expr: an Expression
ty: a Type
ident: an IDENTIFIER_OR_KEYWORD
path: a TypePath style path
tt: a TokenTree (a single token or tokens in matching delimiters (), [], or {})
meta: an Attr, the contents of an attribute
lifetime: a LIFETIME_TOKEN
vis: a possibly empty Visibility qualifier
literal: matches -?LiteralExpression

有個好網站叫 cheats.rs,在這裡解釋了它們,並且為每一種 fragment 給出範例。

然而對於大多數巨集,你只會用到 expridentttident 表示識別字,用於變數或函式名稱。tt 表示標記樹 (Token tree),和任何型別的輸入。讓我們嘗試用前兩者寫個簡單的巨集。

macro_rules! check {
    ($input1:ident, $input2:expr) => {
        println!(
            "Is {:?} equal to {:?}? {:?}",
            $input1,
            $input2,
            $input1 == $input2
        );
    };
}

fn main() {
    let x = 6;
    let my_vec = vec![7, 8, 9];
    check!(x, 6);
    check!(my_vec, vec![7, 8, 9]);
    check!(x, 10);
}

所以這將接受一個 ident (像是變數名)和一個表達式,看看它們是否相同。印出:

Is 6 equal to 6? true
Is [7, 8, 9] equal to [7, 8, 9]? true
Is 6 equal to 10? false

而這裡有一個巨集,它接受輸入 tt,然後把它印出來。它會先使用叫做 stringify! 的巨集做出字串。

macro_rules! print_anything {
    ($input:tt) => {
        let output = stringify!($input);
        println!("{}", output);
    };
}

fn main() {
    print_anything!(ththdoetd);
    print_anything!(87575oehq75onth);
}

印出:

ththdoetd
87575oehq75onth

但如果我們給它一些帶有空格、逗號等的東西,它就不會印出來了。它會認為我們給它不止一個元素或額外的資訊,所以它會感到困惑。

這就是巨集開始變得困難的地方。

要一次提供給巨集多個元素,我們必須使用不同的語法。不是原先的 $input,而是要用 $($input1),*。這意味著用逗號分隔的零或更多(這就是 * 的意思)元素。如果你想要一個或多個,要改用 + 而不是 *

現在我們的巨集看起來像這樣:

macro_rules! print_anything {
    ($($input1:tt),*) => {
        let output = stringify!($($input1),*);
        println!("{}", output);
    };
}


fn main() {
    print_anything!(ththdoetd, rcofe);
    print_anything!();
    print_anything!(87575oehq75onth, ntohe, 987987o, 097);
}

所以它接受任何用逗號隔開的標記樹,並使用 stringify! 把它變成字串,再印出來。印出:

ththdoetd, rcofe

87575oehq75onth, ntohe, 987987o, 097

如果我們使用 + 而不是 *,它會給出錯誤,因為其中一次呼叫時我們沒有給它輸入。所以 * 是個比較安全一點的選擇。

所以現在我們可以開始見識到巨集的威力了。在接下來的範例中,我們實際上可以做出我們自己的函式:

macro_rules! make_a_function {
    ($name:ident, $($input:tt),*) => { // 首先你給它函式一個名字, 然後它檢查其它所有東西
        fn $name() {
            let output = stringify!($($input),*); // 它讓其它所有東西變成字串
            println!("{}", output);
        }
    };
}


fn main() {
    make_a_function!(print_it, 5, 5, 6, I); // 我們想要函式呼叫 print_it() 來印出我們給的其它所有東西
    print_it();
    make_a_function!(say_its_nice, this, is, really, nice); // 這裡一樣但是我們改了函式名
    say_its_nice();
}

印出:

5, 5, 6, I
this, is, really, nice

所以現在我們可以開始瞭解其他的巨集了。你可以見到,我們已經使用的一些巨集相當簡單。這裡是我們過去常用來寫入檔案的 write! 巨集:

#![allow(unused)]
fn main() {
macro_rules! write {
    ($dst:expr, $($arg:tt)*) => ($dst.write_fmt($crate::format_args!($($arg)*)))
}
}

要使用它時,你要輸入這些:

  • 一個表達式 (expr) 用來得到變數名 $dst
  • 之後的所有東西。如果它寫的是 $arg:tt,那麼它只會接受一個元素,但因為它寫的是 $($arg:tt)*,所以它可以接受零、一個或者任意多個。

然後它接受 $dst,並對它呼叫了叫做 write_fmt 的方法。在那裡面,它使用了另一個叫做 format_args! 的巨集來接受所有的 $($arg)*,或者說我們放進去的全部引數。

現在我們來看一下 todo! 這個巨集。當你想讓程式能編譯但你的程式碼還沒寫出來時,這就是你會用到的那個巨集。看起來像這樣:

#![allow(unused)]
fn main() {
macro_rules! todo {
    () => (panic!("not yet implemented"));
    ($($arg:tt)+) => (panic!("not yet implemented: {}", $crate::format_args!($($arg)+)));
}
}

這個有兩個選項:你可以輸入 (),也可以輸入一些標記樹 (tt)。

  • 如果你輸入的是 (),它只是使用加上訊息的 panic!。所以其實你可以直接寫 panic!("not yet implemented"),而不是 todo!,結果也一樣。
  • 如果你輸入一些引數,它會嘗試印出它們。你可以見到裡面有同樣的 format_args! 巨集,它的工作原理和 println! 一樣。

所以如果你寫成這樣,一樣也行得通:

fn not_done() {
    let time = 8;
    let reason = "lack of time";
    todo!("Not done yet because of {}. Check back in {} hours", reason, time);
}

fn main() {
    not_done();
}

會印出:

thread 'main' panicked at 'not yet implemented: Not done yet because of lack of time. Check back in 8 hours', src/main.rs:4:5

在巨集裡面你甚至可以呼叫相同的巨集。這裡是這樣的範例:

macro_rules! my_macro {
    () => {
        println!("Let's print this.");
    };
    ($input:expr) => {
        my_macro!();
    };
    ($($input:expr),*) => {
        my_macro!();
    }
}

fn main() {
    my_macro!(vec![8, 9, 0]);
    my_macro!(toheteh);
    my_macro!(8, 7, 0, 10);
    my_macro!();
}

這個巨集接受 ()、或一個表達式、或很多個表達式都可以。但是不論你放了什麼,它都會忽略所有的表達式,並且最後只呼叫 my_macro!()。所以四次輸出都只是 Let's print this

dbg! 巨集中也可以看到同樣的情況,也就是呼叫自己。

#![allow(unused)]
fn main() {
macro_rules! dbg {
    () => {
        $crate::eprintln!("[{}:{}]", $crate::file!(), $crate::line!()); // $crate 的意思是指本身所在的 crate.
    };
    ($val:expr) => {
        // 這裡 `match` 的使用是有意的因為它影響了暫存變數的
        // 生命週期 - https://stackoverflow.com/a/48732525/1063961
        match $val {
            tmp => {
                $crate::eprintln!("[{}:{}] {} = {:#?}",
                    $crate::file!(), $crate::line!(), $crate::stringify!($val), &tmp);
                tmp
            }
        }
    };
    // 單一引數的後緣逗號會被忽略
    ($val:expr,) => { $crate::dbg!($val) };
    ($($val:expr),+ $(,)?) => {
        ($($crate::dbg!($val)),+,)
    };
}
}

eprintln!println! 相同,除了它印出到 io::stderr 而不是 io::stdout。當然也有個 eprint! 印出時不會加上換行。

所以我們可以自己去試一試。

fn main() {
    dbg!();
}

這與第一分支相匹配,所以它會用 file!line! 巨集印出檔名和行數。印出 [src/main.rs:2]

讓我們用這個來試試:

fn main() {
    dbg!(vec![8, 9, 10]);
}

這將會匹配到下一個分支,因為它是個表達式。然後它將把輸入叫做 tmp 並使用這段程式碼:$crate::eprintln!("[{}:{}] {} = {:#?}", $crate::file!(), $crate::line!(), $crate::stringify!($val), &tmp);。所以它會用 file!line! 來印出,再把 $val 做成 String,並且用 {:#?} 來給 tmp 做漂亮列印。所以對於我們的輸入,它會寫成這樣:

[src/main.rs:2] vec![8, 9, 10] = [
    8,
    9,
    10,
]

剩下的部分,即使你加了額外的逗號,它也只是對自己呼叫 dbg!

正如你所見,巨集是非常複雜的!通常你只想讓巨集自動做些簡單函式無法做得很好的事情。學習巨集的最佳方法就是看看其他巨集的例子。沒有多少人能夠快速寫出巨集而不出問題。所以在 Rust 中,不用認為你需要知道巨集的一切才能知道如何撰寫。但如果你讀了其他巨集,並稍加修改,你就可以很容易地借用它們的威力。之後你可能就會開始習慣寫出自己的巨集。

第二部 - 電腦上的 Rust

你見到了我們可以只使用 Playground 就學習到 Rust 裡的幾乎任何東西。但到目前為止如果你已經學了這麼多,現在你也許會想要在你的電腦上使用 Rust。總有一些事情是你沒辨法用 Playground 做到的,比如使用檔案或在多個檔案中的程式碼。也有一些其它東西需要在電腦上安裝 Rust 的是輸入功能和 flags。但最重要的事是在你的電腦上有了 Rust,你可以使用 Crate。我們已經學過 Crate ,但在 Playground 中你只能使用最流行的那一個。但在你的電腦上有了 Rust,你就可以在你的程式中使用任何 Crate。