Try   HackMD

Crust of Rust: Declarative Macros

直播錄影

  • 主機資訊
    ​​​​wilson@wilson-HP-Pavilion-Plus-Laptop-14-eh0xxx ~> neofetch --stdout
    ​​​​wilson@wilson-HP-Pavilion-Plus-Laptop-14-eh0xxx 
    ​​​​----------------------------------------------- 
    ​​​​OS: Ubuntu 22.04.3 LTS x86_64 
    ​​​​Host: HP Pavilion Plus Laptop 14-eh0xxx 
    ​​​​Kernel: 6.2.0-37-generic 
    ​​​​Uptime: 4 mins 
    ​​​​Packages: 2368 (dpkg), 11 (snap) 
    ​​​​Shell: bash 5.1.16 
    ​​​​Resolution: 3840x2160 
    ​​​​DE: GNOME 42.9 
    ​​​​WM: Mutter 
    ​​​​WM Theme: Adwaita 
    ​​​​Theme: Yaru-dark [GTK2/3] 
    ​​​​Icons: Yaru [GTK2/3] 
    ​​​​Terminal: gnome-terminal 
    ​​​​CPU: 12th Gen Intel i5-12500H (16) @ 4.500GHz 
    ​​​​GPU: Intel Alder Lake-P 
    ​​​​Memory: 4322MiB / 15695MiB 
    
  • Rust 編譯器版本 :
    ​​​​wilson@wilson-HP-Pavilion-Plus-Laptop-14-eh0xxx ~/CrustOfRust> rustc --version
    ​​​​rustc 1.70.0 (90c541806 2023-05-31) (built from a source tarball)
    

Introduction

0:00:00

In this second Crust of Rust video, we cover declarative macros, macro_rules!, by re-implementing the vec! macro from the standard library. As part of that, we cover not only how to write these, but some of the gotchas and tricks you'll run into, and some common use-cases.

The vec macro

0:01:35

本次實作內容可以在標準函式庫 - Macro std::vec :

  • 定義 vec! 巨集 :
    ​​​​macro_rules! vec { ​​​​ () => { ... }; ​​​​ ($elem:expr; $n:expr) => { ... }; ​​​​ ($($x:expr),+ $(,)?) => { ... }; ​​​​}
    macro_rules! : 定義新巨集的起手式。
    vec : 巨集的名稱
    {...} : 模式比對,傳到巨集內的參數比傳到函式內的參數寬鬆。舉例來說,一般函式變數後面跟著一個 Rust 型別,巨集變數後面跟著一個語法型別 (identifier, expression, item, block, name of type, path to a type, path to a module等等);一般函式採用一般模式比對,而巨集的模式比對是採用語法模式比對,使用上兩者也有差異。
  • 使用 vec! 巨集 :
    ​​​​let v = vec![1; 3]; ​​​​assert_eq!(v, [1, 1, 1]);

本影片的重點是要教您巨集是怎麼實作的、巨集可以做什麼,以及如何設計巨集,而不是探討各種語法型別。

The Little Book of Rust Macros

0:04:08

The Little Book of Rust Macros 很好的解釋了巨集的許多語法,巨集一些奇怪的地方,書的 第 4 章 還提及到巨集許多非常方便的模式,第 4 章 章一部分的模式可以在我們本次的實作中看到。

Macro syntax and hygiene

0:05:17

開始建置 Rust 專案 :

$ cargo new --lib vecmac
$ cd vecmac
$ vim src/lib.rs

:bulb: 函式庫更容易獨立開發,養成製作函式庫的習慣是件好事。

巨集括號的使用:

macro_rules! avec { () => {} // expands to nothing } macro_rules! avec1 [ () => {} ] // 加上分號才合法 macro_rules! avec2 ( () => {} ) // 加上分號才合法 // 呼叫方式 avec!(); avec![]; avec!{}; // 移除分號才合法

:question: 0:06:18
Q: 為什麼有時候要分號有時候不能有分號 ?
A: 應該是因為 macro_rules! 本身也是巨集的原因有關 ?

測試括號的使用 :

$ cargo check
    Checking vecmac v0.1.0 (/home/wilson/CrustOfRust/vecmac)
error: macros that expand to items must be delimited with braces or followed by a semicolon
 --> src/lib.rs:6:1
  |
6 | / [
7 | |     () => {}
8 | | ]
  | |_^
  |
help: change the delimiters to curly braces
  |
6 + {
7 |     () => {}
8 + }
  |
help: add a semicolon
  |
8 | ];
  |  +

error: macros that expand to items must be delimited with braces or followed by a semicolon
  --> src/lib.rs:10:1
   |
10 | / (
11 | |     () => {}
12 | | )
   | |_^
   |
help: change the delimiters to curly braces
   |
10 + {
11 |     () => {}
12 + }
   |
help: add a semicolon
   |
12 | );
   |  +

error: expected item, found `;`
  --> src/lib.rs:16:8
   |
16 | avec!{};
   |        ^ help: remove this semicolon

巨集主要是在寫輸入語法上的一堆模式。Rust 語法樹上的模式,它們不是參數列表 :

macro_rules! avec { // 第一個模式 ($arg1:ty, $arg2:expr, $arg3:path) => {}; // 你可以用這種方式表達參數 // 第二個模式 ($arg1:ty => $arg2:expr; $arg3:path) => {}; // 也可以用這種方式表達參數 // 因為巨集用的是語法模式 } avec!{ u32 => x.foo; std::path // 符合第二個模式 }

這樣是可以通過編譯器的檢查的:

$ cargo check Checking vecmac v0.1.0 (/home/wilson/CrustOfRust/vecmac) Finished dev [unoptimized + debuginfo] target(s) in 0.05s

如果直接編譯 u32 => x.foo; std::path 是不會過的,因為這不屬於 Rust 語法,但如果你在巨集內使用 u32 => x.foo; std::path 則是可以編譯成功的。因為這些 token 在巨集裡面都是可以語法分析的。注意到,巨集的輸入要可以語法分析輸出則要 Rust 語法

你可以用 cargo-expand 來展開巨集:

$ cargo install cargo-expand $ cargo expand Checking vecmac v0.1.0 (/home/wilson/CrustOfRust/vecmac) Finished dev [unoptimized + debuginfo] target(s) in 0.05s #![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2021::*; #[macro_use] extern crate std;

目前之所以沒看到 avec!{...} 的展開結果是因為我們第二個模式的輸出是 {}。不過上面的訊息我們可以看到,Rust 注入 prelude 到每個 Rust 程式。

寫一個類似 typedef 的程式測試輸出:

macro_rules! avec { ($arg1:ty => $arg2:ident) => { type $arg2 = $arg1; }; } avec!{ u32 => alsou32 }

再次測試巨集展開功能:

$ cargo expand Checking vecmac v0.1.0 (/home/wilson/CrustOfRust/vecmac) warning: type `alsou32` should have an upper camel case name --> src/lib.rs:8:12 | 8 | u32 => alsou32 | ^^^^^^^ help: convert the identifier to upper camel case: `Alsou32` | = note: `#[warn(non_camel_case_types)]` on by default Finished dev [unoptimized + debuginfo] target(s) in 0.05s #![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2021::*; #[macro_use] extern crate std; type alsou32 = u32;

這次就可以看到巨集展開的結果了。

Q: When you say syntactically valid, do you mean it is valid rust grammar or just valid rust tokens?
A: 它必須是合法的文法,($arg1:ty => $arg2:ident) 可以,($arg1:ty -> $arg2:ident) 卻不行的原因是因為 Rust 文法不允許模式內使用 -> ,之所以不允許使用 ->(以及其他會造成問題的 tokens) 是因為會帶來斷詞歧異。當遇到這種問題時,rust-analyzer 會顯示以下的資訊:
image
你可以根據它給的訊息選擇別的 token 來使用。比如說你可以選擇 as-> 合理多了,因為這裡是在定義類別:

macro_rules! avec { ($arg1:ty as $arg2:ident) => { type $arg2 = $arg1; }; } avec!{ u32 as alsou32 }

輸出結果:

$ cargo expand Checking vecmac v0.1.0 (/home/wilson/CrustOfRust/vecmac) warning: type `alsou32` should have an upper camel case name --> src/lib.rs:8:12 | 8 | u32 as alsou32 | ^^^^^^^ help: convert the identifier to upper camel case: `Alsou32` | = note: `#[warn(non_camel_case_types)]` on by default Finished dev [unoptimized + debuginfo] target(s) in 0.05s #![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2021::*; #[macro_use] extern crate std; type alsou32 = u32;

故意不匹配模式:

macro_rules! avec { ($arg1:ty as $arg2:ident) => { type $arg2 = $arg1; }; } avec!{ u32 => alsou32 }

會出現以下錯誤:

error: no rules expected the token `=>` --> src/lib.rs:8:9 | 1 | macro_rules! avec | -------------------- when calling this macro ... 8 | u32 => alsou32 | ^^ no rules expected this token in macro call

Hygiene

0:12:39

你定義在巨集內部的 identifier 跟在巨集外部的 identifier 在兩個不同的宇宙。

macro_rules! avec { () => { let x = 42; }; } fn foo() { avec!(); x + 1; }

編譯器會檢查到以下錯誤:

$ cargo check Checking vecmac v0.1.0 (/home/wilson/CrustOfRust/vecmac) error[E0425]: cannot find value `x` in this scope --> src/lib.rs:11:5 | 11 | x + 1; | ^ not found in this scope

這樣子也不行:

macro_rules! avec { () => { x += 1; }; } fn foo() { let mut x = 42; avec!(); }

也是過不了編譯器的檢查:

cargo check Checking vecmac v0.1.0 (/home/wilson/CrustOfRust/vecmac) error[E0425]: cannot find value `x` in this scope --> src/lib.rs:4:9 | 4 | x += 1; | ^ not found in this scope ... 11 | avec!(); | ------- in this macro invocation | = note: this error originates in the macro `avec` (in Nightly builds, run with -Z macro-backtrace for more info)

這種不允許巨集內部 identifier 直接存取巨集外部的 identifier 性質叫做:hygiene。在本次實作中,我們不在乎這個性質。

你必須將 identifier 作為巨集的輸入:

macro_rules! avec { ($x:ident) => { $x += 1; }; } fn foo() { let mut x = 42; avec!(x); // 想要跨越宇宙,你必須明確地傳入 identifier }

這次執行 cargo check 即可通過編譯器的檢查了。

The empty vector

0:16:42

這次採 test-driven development,首先,從沒有任何的模式開始設計程式:

macro_rules! avec { } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); }

因為沒有任何模式,所以編譯器會檢查到以下錯誤:

$ cargo check Checking vecmac v0.1.0 (/home/wilson/CrustOfRust/vecmac) error: unexpected end of macro invocation --> src/lib.rs:1:1 | 1 | / macro_rules! avec 2 | | { 3 | | } | |_^ missing tokens in macro arguments ...

但這個錯誤訊息是不好的,因為我們預期得到的錯誤提示是 "巨集內部沒有任何模式",然而我們卻得到 "missing tokens" 的錯誤提示。至於為什麼會給我們 "missing tokens" 的錯誤提示是因為 macro_rules! 本身也是巨集,{} 內部是模式,然而這裡的模式沒有任何的 token,所以才顯示 "missing tokens" 的錯誤提示。

接著開始在巨集裡面加簡單的模式和樣板:

#[macro_export] macro_rules! avec { + () => { + Vec::new() + }; }

#[macro_export] 讓使用這個函式庫的人可以呼叫這個巨集,加上 #[macro_export] 有點像是讓巨集變成 pub,如果沒有 #[macro_export],巨集將無法從任何依賴我們的 crate 的函式庫中呼叫。

目前程式碼
#[macro_export] macro_rules! avec { () => { Vec::new() }; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); }

再來驗證程式碼,可通過 test case:

$ cargo check
...
running 1 test
test empty_vec ... ok
...

嘗試展開 test case 部份的巨集 :

$ cargo expand --lib --tests Compiling vecmac v0.1.0 (/home/wilson/CrustOfRust/vecmac) Finished test [unoptimized + debuginfo] target(s) in 0.05s #![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2021::*; #[macro_use] extern crate std; ... fn empty_vec() { let x: Vec<u32> = Vec::new(); if !x.is_empty() { ::core::panicking::panic("assertion failed: x.is_empty()") } } ...

Q: what about ownership in the case of $x example
A: 當你將 identifier 傳到巨集內部,你只是傳了這個 identifer 的存取權,並沒有把所有權也移交到巨集內部。這行為並不像將參數傳到函式內部,將 identifier 傳到巨集內部並沒有移動 identifier 的值。真正牽涉到所有權的部份是展開的巨集對那個 identifer 做了什麼,它的操作可能移動了值,也可能沒有移動值。

Non-empty vectors

0:19:26

接下來嘗試讓巨集支援讓 Vec 可以加入單一元素:

#[macro_export] macro_rules! avec { ... + ($element:expr) => { // expr : 運算式型態。 + let mut vs = Vec::new(); + vs.push($element); + vs + }; }

並且新增測單個元素的功能 :

#[test] fn single() { let x: Vec<u32> = avec![42]; assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); }

:pencil2: 陳述式與表達式 :

  • 陳述式(Statements)是進行一些動作的指令,且不回傳任何數值。
  • 表達式(Expressions)則是計算並產生數值。
fn main() { let i = { // This is a statement let j = 69; // This is an expression j + 1 }; }
目前程式碼
#[macro_export] macro_rules! avec { () => { Vec::new() }; ($element:expr) => { // expr : 運算式型態。 let mut vs = Vec::new(); vs.push($element); vs }; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn single() { let x: Vec<u32> = avec![42]; assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); }

執行 cargo check 雖然會通過,但如果嘗試測試,會得到一長串的錯誤:

錯誤訊息
$ cargo test Compiling vecmac v0.1.0 (/home/wilson/CrustOfRust/vecmac) error: expected expression, found `let` statement --> src/lib.rs:8:9 | 8 | let mut vs = Vec::new(); | ^^^ ... 17 | let x: Vec<u32> = avec![42]; | --------- in this macro invocation | = note: this error originates in the macro `avec` (in Nightly builds, run with -Z macro-backtrace for more info) error: macro expansion ignores token `vs` and any following --> src/lib.rs:9:9 | 9 | vs.push($element); | ^^ ... 17 | let x: Vec<u32> = avec![42]; | --------- caused by the macro expansion here | = note: the usage of `avec!` is likely invalid in expression context error: expected expression, found statement (`let`) --> src/lib.rs:8:9 | 8 | let mut vs = Vec::new(); | ^^^^^^^^^^^^^^^^^^^^^^^ ... 17 | let x: Vec<u32> = avec![42]; | --------- in this macro invocation | = note: variable declaration using `let` is a statement = note: this error originates in the macro `avec` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0658]: `let` expressions in this position are unstable --> src/lib.rs:8:9 | 8 | let mut vs = Vec::new(); | ^^^^^^^^^^^^^^^^^^^^^^^ ... 17 | let x: Vec<u32> = avec![42]; | --------- in this macro invocation | = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information = note: this error originates in the macro `avec` (in Nightly builds, run with -Z macro-backtrace for more info) warning: trailing semicolon in macro used in expression position --> src/lib.rs:8:32 | 8 | let mut vs = Vec::new(); | ^ ... 17 | let x: Vec<u32> = avec![42]; | --------- in this macro invocation | = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release! = note: for more information, see issue #79813 <https://github.com/rust-lang/rust/issues/79813> = note: `#[warn(semicolon_in_expressions_from_macros)]` on by default = note: this warning originates in the macro `avec` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0308]: mismatched types --> src/lib.rs:8:9 | 8 | let mut vs = Vec::new(); | ^^^^^^^^^^^^^^^^^^^^^^^ expected `Vec<u32>`, found `bool` ... 17 | let x: Vec<u32> = avec![42]; | -------- --------- in this macro invocation | | | expected due to this | = note: expected struct `Vec<u32>` found type `bool` = note: this error originates in the macro `avec` (in Nightly builds, run with -Z macro-backtrace for more info)

之所以會得到這麼長串的錯誤是因為我們告訴 Rust 將 avec[42] 這個表達式展開成三個陳述式 (Line 8 到 Line 10),但你不能在 Rust 這樣做,嘗試展開這個巨集看看:

$ cargo expand --lib --tests
...
fn single() {
    let x: Vec<u32> = let mut vs = Vec::new();
...

這語法完全錯誤,我們必須樣板變成一個區塊,這樣就可以回傳表達式並且指派值給 x:

#[macro_export] macro_rules! avec { () => { Vec::new() }; - ($element:expr) => { + ($element:expr) => {{ + // 將三個陳述式前後用大括號包起來 + // 即可變成一個區塊 + // 這樣就把陳述式成功轉成表達式了 let mut vs = Vec::new(); vs.push($element); vs - }; + }}; } ... #[test] fn single() { + // 這裡就可以將表達式的值指派到 x 變數了 let x: Vec<u32> = avec![42]; assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); }
目前程式碼
#[macro_export]
macro_rules! avec
{
    () => {
        Vec::new()
    };
    ($element:expr) => {{
        let mut vs = Vec::new();
        vs.push($element);
        vs
    }};
}

#[test]
fn empty_vec() 
{
    let x: Vec<u32> = avec![];
    assert!(x.is_empty());
}

#[test]
fn single() 
{
    let x: Vec<u32> = avec![42];
    assert!(!x.is_empty());
    assert_eq!(x.len(), 1);
    assert_eq!(x[0], 42);
}

再次展開巨集可以看到符合 Rust 語法的陳述式了:

$ cargo expand --lib --tests
...
fn single() {
    let x: Vec<u32> = {
        let mut vs = Vec::new();
        vs.push(42);
        vs
    };
...

程式也順利通過 test case 了:

$ cargo test
...
running 2 test
test single ... ok
...

之所以在有些巨集你會在樣板看到兩層大括號是因為外層的大括號的是 macro_rules 語法要求我們的,而內層的括號是用來將陳述式轉成表達式的方法。至於外層的大括號要存在的原因是你可能想要呼叫一個巨集展開成多個 items/函式/模組 (不要用到 let 即可),這時候你不會想讓它是個區塊,你不想讓它預設是個區塊,你需要外層的大括號跟 Rust 說所有東西都在這裡,但因為我們這裡要要將它轉成區塊,所以額外加了一個大括號 (內層的大括號)。

Q: can you use a macro call inside the macro? vec![$element] ?
A: 可以

:question: 0:23:48
Q: How is the macro_rules! marco implemented? :O
A: macro_rules! 巨集不是一般的巨集,它是個 Procedural Macros。我們沒有辦法使用 macro_rules! 語法來獲得 avec{...} 這種表示法,因為我們使用語法定義的所有巨集都會變成必須立即使用分隔符號呼叫的東西 (avec![]),巨集名稱!identifier括號(macro_rules! avec{}) 這種特別的語法並沒有被 Rust 支援。所以 macro_rules! 本身也必須是個巨集才能讓 Rust 支援這種特別的語法。所以我們不能用 macro_rules! 實作 avec 巨集, 我們只能用 macro_rules! 巨集來實作 avec 巨集。

Q: Should the no argument version of the macro use double braces? or doesn't it matter? (Line 4 - Line 6)
A: 不需要,因為展開得到的是表達式而不是一堆陳述式。

Macros v2

0:25:50

Q: Are macros V2 still coming to Rust ? If so what will change ?
A: 是的,有一個新的 macro_rules! 的提案,要把 macro_rules! 改叫做 marco! 或是 macros。V2 會是一個強大的版本,該版本有著些微不同關於 hygiene 的保證,該版本也會更好地使用模組系統以及新的陳述式等等。但它們不能直接將 marco_rules! 直接改名稱,因為很多函式庫都已經用了這個名稱,所以會用新名稱來定義一般巨集。

Macro delimiters

0:26:34

Q: How does the square brackets work in the instantiation? They're not referenced in the macro definition..
A: 當你定義 macro_rules! 時,如一開始說的,使用者可以自由地選擇分隔符號: {}, [], ()。你也不能強迫 caller 使用哪一種分隔符號。每種分隔符號都是合法且有相同意義的。

Q: oh jon, the right hand side of the macro can be written with ({ and }), if that helps make it more clear
A: 除了最外層在用的分隔符號,連模式和樣板的括號都可以自由地選擇分隔符號。但樣板裡面包區塊這種情形在函式庫還是以兩層的大括號居多。當你 vim 存檔時 0:27:32,rust formatter 也會將外層的非大括號改成大括號。(:question: 還要查 Jon 是用什麼 IDE 才有支援這個功能,連結)

Declarative vs procedural macros

0:27:54

Q: Can you have access to the custom compile time stuff like access to some file or something like with proc macros?
A: 程序式巨集讓你基本上可以寫一個 Rust 程式。macro_rules! 則不讓你這麼做,因為它完全是宣告性的,它基本上只能做替換,將語法合法的 Rust 語法樹替換成合法 Rust 程式或 Rust 語法樹。

:question: 0:28:22
Q: Can reflection be used to return an expression?
A: 不知道在問什麼, 不過巨集無法存取類型資訊等內容。宣告型的巨集做的是就是做替換,如果你想要更多的存取 (ex. introspection),不但宣告型的巨集做不到,連程序式的巨集也做不到。

Q: can proc macros take identifiers before the argument block?
A: 宣告型巨集不行,但程序式巨集可以。使用程序式巨集,你可以寫你自己的定義的語法,你不會受到與 macro_rules 相同的要求的限制。

Q: Delimiter ambiguity doesn't seem rusty at all
B: 是有一點奇怪,Jon 不確定是否 macro v2 仍會有這東西。

Q: Could you quickly describe the differences between the types of macros?
A: 我們前面看到的宣告型的巨集,給一個輸入模式,以及要替換成的程式碼。程序式巨集則是採用 Rust 語法 streams 作為輸入或 token streams 作為輸入並產生不同的 token streams 來取代它的程序,程序式巨集更具表現力,但編寫起來也很複雜。程序式巨集也可以加屬性例如 #[derive],這也是宣告式巨集做不到的。

Repeated macro arguments

0:30:15

接下來嘗試讓巨集支援讓 Vec 可以加入兩個元素,在此之前,先新增一個 test case:

#[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); }

將滑鼠移動到 avec![42, 43]; 可以看到以下錯誤:
image

接著修改巨集的部份, 首先做一個土炮版本的巨集來支援兩個元素:

#[macro_export] macro_rules! avec { () => { Vec::new() }; ($element:expr) => {{ let mut vs = Vec::new(); vs.push($element); vs }}; + ($e1:expr, $e2:expr) => {{ + let mut vs = Vec::new(); + vs.push($e1); + vs.push($e2); + vs + }}; }
目前程式碼
#[macro_export] macro_rules! avec { () => { Vec::new() }; ($element:expr) => {{ let mut vs = Vec::new(); vs.push($element); vs }}; ($e1:expr, $e2:expr) => {{ let mut vs = Vec::new(); vs.push($e1); vs.push($e2); vs }}; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn single() { let x: Vec<u32> = avec![42]; assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); } #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); }

當你想要支援任意或更多的元素的時候,顯然這不會是個好方法。

再來做一個聰穎版本的巨集來支援兩個元素:

#[macro_export] macro_rules! avec { () => { Vec::new() }; ($($element:expr),+) => {{ let mut vs = Vec::new(); $(vs.push($element);)+ // $(vs.push($element);)* 可以重複多次。 vs }}; }

模式支援重複,使用方法是在模式內多包一層 $(),並根據你的需求在 ) 決定元素是否要用分隔符號相隔,要的話是使用那一種分隔符號,模式要符合的次數 (這裡的符號意義跟正規表示法一樣),以下是一些使用範例:

模式 意義
$( ... )* 符合 0 次或更多次,沒有分隔符號
$( ... ),* 符合 0 次或更多次,以逗號分隔
$( ... );* 符合 0 次或更多次,以分號分隔
$( ... )+ 符合 1 次或更多次,沒有分隔符號
$( ... ),+ 符合 1 次或更多次,以逗號分隔
$( ... );+ 符合 1 次或更多次,以分號分隔
$( ... )? 符合 0 次或 1 次,沒有分隔符號
$( ... ),? 符合 0 次或 1 次,以逗號分隔
$( ... );? 符合 0 次或 1 次,以分號分隔

而樣板需要支援重複也是將要支援重複功能的那行多包一層 $()* (*,+, ? 都可以用來表示該行需要重複,但每個符號有著不同的重複次數限制),記得如果是要包陳述式的話,連 ; 都要包起來,否則巨集展開時,重複部份的陳述式之間會沒有 ; 相隔。

一個模式可以重複多次,在樣板內,只要每次遇到 $()*,它就會去尋找與哪個模式匹配
並拉出變數。

展開程式碼即可得到以下結果:

$ cargo expand --lib --tests ... fn double() { let x: Vec<u32> = { let mut vs = Vec::new(); vs.push(42); vs.push(43); // vs.push(42); 得到第二組重複的程式碼 // vs.push(43); vs }; ...

執行 cargo test 也順利通過 double() 的測試了。

Q: I guess you could do that and then generate the macro with another macro_rules
A: 你必須非常小心的使用 macro_rules! 生成 macro_rules!,因為編譯器有時候會混淆,等等我們會看到這種情形的範例。

:bulb: fun fact: you can use any single rust token, you can have an "else" separated list. (from chatroom)

#[macro_export] macro_rules! avec { () => { Vec::new() }; ($($element:item)else+) => {{ // item 型態可以,expr, ty ...等型態不行 let mut vs = Vec::new(); $(vs.push($element);)* vs.push($e2); vs }}; }

Q: so, what happens when you use both variables in a single repetition?
A: 請看以下程式碼:

錯誤示範程式碼
#[macro_export] macro_rules! avec { () => { Vec::new() }; ($($element:expr),+; $($x:ident),*) => {{ let mut vs = Vec::new(); $(let $x = vs.push($element);)* vs }}; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn single() { let x: Vec<u32> = avec![42;]; // 符合 1 次模式前半部,符合 0 次模式後半部, 這樣展開的時候會有問題 assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); } #[test] fn double() { let x: Vec<u32> = avec![42, 43;]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); }

雖然執行 cargo check 編譯器會過,但當執行測試時卻出現以下錯誤:

$ cargo test Compiling vecmac v0.1.0 (/home/wilson/CrustOfRust/vecmac) error: meta-variable `x` repeats 0 times, but `element` repeats 1 time --> src/lib.rs:9:10 | 9 | $(let $x = vs.push($element);)* | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: meta-variable `x` repeats 0 times, but `element` repeats 2 times --> src/lib.rs:9:10 | 9 | $(let $x = vs.push($element);)* |

要讓前後半部符合次數一致,才可以這樣使用

範例程式碼
#[macro_export] macro_rules! avec { () => { Vec::new() }; ($($element:expr),+; $($x:ident),*) => {{ let mut vs = Vec::new(); $(let $x = vs.push($element);)+ vs }}; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn single() { let x: Vec<u32> = avec![42; foo]; // 符合 1 次模式前半部,符合 0 次模式後半部, 這樣展開的時候會有問題 assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); } #[test] fn double() { let x: Vec<u32> = avec![42, 43; foo, bar]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); }

這樣 cargo check 以及 cargo test 都不會報錯了。

Q: can you get the element count and plug it in Vec::with_capacity()
A: 等等就會提及了。

Q: How would I define something like the format macro that check if the number of brackets matches the number of expressions after the string?
A: format! 巨集不是 macro_rules!macro_rules! 在某種程度上限制了你可以定義的內容。你可以在 macro_rules! 中完成很多事情,但是像一些更複雜的事情你需要放到程序式巨集去做。

Q: + is one or more, * is zero or more?
A: 是的。

Q: @jonhoo any idea where that macro language got its inspiration from? feels a bit magic
A: Jon 不知道 $() 從哪裡來的,不過這讓人想起 (reminiscent) 了正規表達式。在正規表達式中,() 是用來分組,+ 則表示符合 () 內模式 1 次或者更多次。至於 $, 從哪裡來的 Jon 不知道

Trailing commas

0:39:49

現在的巨集已經可以支援 0 個以上元素放到 Vec 裡面了。

目前程式碼
#[macro_export] macro_rules! avec { () => { Vec::new() }; ($($element:expr),+) => {{ let mut vs = Vec::new(); $(vs.push($element);)+ vs }}; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn single() { let x: Vec<u32> = avec![42]; assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); } #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); }

接下來要做的是處理尾隨逗號。首先新增一個 test case,(這個 case 尚未實作完):

#[test] fn trailing() { let x: Vec<u32> = avec![ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27 ]; }

:pencil2: avec! 巨集之所以那麼多元素是因為 Jon 想要展示當存檔時,Rust format 會因為 Line 長度過長會自動換行 (:question: 還要查 Jon 是用什麼 IDE 才有支援這個功能,連結)。

當你有這麼長的東西,你通常想要在最後一個元素加上一個尾隨逗號,這應該要是可行的,像這樣:

#[test] fn trailing() { let _: Vec<&'static str> = avec![ "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", ]; }

但目前的模式並不支援這種輸入,解法有兩種:

  1. ($($element:expr),+,) 這意思是說它會被用 , 分開,然後最後一個元素後面會有一個 ,
  2. ($($element:expr,)+) 這意思是說它不會被任何東西分開,但每個模式後面都必須有 ,

但上面這兩種解法有一個問題,就是現在最後一個元素後面一定要加 ,,如果不加 , 反而不行。所以真正的解法應該是改成 : ($($element:expr),+ $(,)?),這模式的意思它會被用 , 分開,然後最後一個元素後面可以 0 或 1 個 ,,注意到 $(,)? 並不會在巨集展開的時候出現在程式碼,這裡只是為了允許最後一個元素後面的逗號可有可無。

Q: Do you actually need the comma before the +, or is it just indicating that the user should use commas as separators?
A: 使用 avec![42, 43] 的情況下你需要 ,,如果沒有 , 表示你的輸入將預期得到一個表達序列,這個序列沒有被任何東西分隔開來。但如果使用的是 avec![42 43],你可以在模式不用加那個 , (white space 視為沒有分隔符號),沒有寫那個 , 並不表示說使用者可以自己決定要用什麼分隔符號,而是元素之間沒有分隔符號。

Q: can you match a specific number like you can with regex braces
A: Jon 認為不行,因為模式用的並不全然是正規表達式,這裡只能用 ,, ?, * 而已。實際情況要自己去查閱書籍。

Q: Can you check repetition's length to print more intuitive error when the two repetetitions are not the same length?
A: 編譯器就會給你足夠的訊息了,不需要再額外做這件事。宣告型巨集只會讓你得到你要得到的,它本身不能給你好的錯誤訊息,你只能調整你的巨集讓編譯器告訴你更好的錯誤訊息。如果你想提供一個非常強大的巨集,其中事情可能會以微妙的方式出錯,你最好使用程序型巨集,因為它讓你更好地控制什麼東西出錯了,你的程式碼忽略了什麼,或者發生什麼錯誤了。

Why are macros useful?

0:44:10

Q: What are the general benefits for defining own macros? They are quite complicated. Performance?
A: 因為巨集很方便,看到以下的應用場景:

trait MaxValue { fn max_value() -> Self; }

這個 trait 可以用在所有的數字類型。
如果沒有巨集的話你要一一為每個數字類型實作這個 trait:

trait MaxValue { fn max_value() -> Self; } impl MaxValue for u32 { fn max_value() -> Self { u32::MAX } } impl MaxValue for i32 { fn max_value() -> Self { i32::MAX } } impl MaxValue for u64 { fn max_value() -> Self { u64::MAX } } impl MaxValue for i64 { fn max_value() -> Self { i64::MAX } }

如果你使用巨集的話,只要寫成這樣:

trait MaxValue { fn max_value() -> Self; } macro_rules! max_impl { ($t:ty) => { impl $crate::MaxValue for $t { fn max_value() ->Self { <$t>::MAX } } } } max_impl!(i32); max_impl!(u32); max_impl!(i64); max_impl!(u64);

這樣的程式碼更加簡潔彈性,這樣如果要要調整程式碼只要調整巨集部份即可。

Q: Is #[derive] class logic written using proc macros?
A: 是的。

Q: What determines if a macro is called with parenthesis vs square bracket?
A: 作為 caller 的你自己決定,像是 vec![1, 2] 其實也可以用 vec!(1, 2)

目前程式碼
#[macro_export] macro_rules! avec { () => { Vec::new() }; ($($element:expr),+ $(,)?) => {{ let mut vs = Vec::new(); $(vs.push($element);)+ vs }}; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn single() { let x: Vec<u32> = avec![42]; assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); } #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); } #[test] fn trailing() { let _: Vec<&'static str> = avec![ "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", ]; }

Vector by repetition

0:47:29

接下來,我們想要再讓巨集支援一個新的模式:

#[macro_export] macro_rules! avec { ... + ($element:expr; $count:expr) => {{ + let mut vs = Vec::new(); + for _ in 0..$count { + vs.push($element); + } + vs + }}; } ...

並新增一個 test case:

#[test] fn clone_2() { let x: Vec<u32> = avec![42; 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); }
目前程式碼
#[macro_export] macro_rules! avec { () => { Vec::new() }; ($($element:expr),+ $(,)?) => {{ let mut vs = Vec::new(); $(vs.push($element);)+ vs }}; ($element:expr; $count:expr) => {{ let mut vs = Vec::new(); for _ in 0..$count { vs.push($element); } vs }}; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn single() { let x: Vec<u32> = avec![42]; assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); } #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); } #[test] fn trailing() { let _: Vec<&'static str> = avec![ "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", ]; } #[test] fn clone_2() { let x: Vec<u32> = avec![42; 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); }

雖然可以通過測試,但有些東西是錯誤的。巨集基本上就是做程式碼替換,如果表達式是常數值那可以,但若表達式是更複雜的型別,這方法將不可行:

#[test] fn clone_2_nonliteral() { let mut y = Some(42); let x: Vec<u32> = avec![y.take().unwrap(); 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); }
目前程式碼
#[macro_export] macro_rules! avec { () => { Vec::new() }; ($($element:expr),+ $(,)?) => {{ let mut vs = Vec::new(); $(vs.push($element);)+ vs }}; ($element:expr; $count:expr) => {{ let mut vs = Vec::new(); for _ in 0..$count { vs.push($element); } vs }}; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn single() { let x: Vec<u32> = avec![42]; assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); } #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); } #[test] fn trailing() { let _: Vec<&'static str> = avec![ "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", ]; } #[test] fn clone_2() { let x: Vec<u32> = avec![42; 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); } #[test] fn clone_2_nonliteral() { let mut y = Some(42); let x: Vec<u32> = avec![y.take().unwrap(); 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); }

測試會出現以下錯誤,展開巨集來看看為什麼會有這個錯誤:

$ cargo test ... ---- clone_2_nonliteral stdout ---- thread 'clone_2_nonliteral' panicked at 'called `Option::unwrap()` on a `None` value', src/lib.rs:63:38 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrac ... $ cargo expand --tests --lib ... fn clone_2_nonliteral() { let mut y = Some(42); let x: Vec<u32> = { let mut vs = Vec::new(); for _ in 0..2 { vs.push(y.take().unwrap()); // 這裡執行到第一次還可以取到數字, } // 但第二次時就只能取到 None 的值 vs }; ... } ...

我們想表達的是,巨集應該是要這樣寫的:

#[macro_export] macro_rules! avec { ... ($element:expr; $count:expr) => {{ let mut vs = Vec::new(); let x = $element; for _ in 0..$count { - vs.push($element); + vs.push(x.clone()); } vs }}; } ...

這樣寫的話可以讓表達式只被執行 evaluate 一次,我們將它的結果記下來,並且在每次要 push 到 Vec 時複製該結果。

目前程式碼
#[macro_export] macro_rules! avec { () => { Vec::new() }; ($($element:expr),+ $(,)?) => {{ let mut vs = Vec::new(); $(vs.push($element);)+ vs }}; ($element:expr; $count:expr) => {{ let mut vs = Vec::new(); let x = $element; for _ in 0..$count { vs.push(x.clone()); } vs }}; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn single() { let x: Vec<u32> = avec![42]; assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); } #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); } #[test] fn trailing() { let _: Vec<&'static str> = avec![ "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", ]; } #[test] fn clone_2() { let x: Vec<u32> = avec![42; 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); } #[test] fn clone_2_nonliteral() { let mut y = Some(42); let x: Vec<u32> = avec![y.take().unwrap(); 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); }

Q: Are macros in rust the answer for people who want OOP like Inheritance?
A: 不是,Jon 認為你一點也不會想要物件導向的巨集。

Macro rules readability

0:51:02

Q: Do you find macro_rules! macro's readable? I find they are hard to understand when theres multiple patternmatches involved
A: macro_rules! 如果在簡單的情況下,它的可讀性良好,但是若超出了比較簡單的範疇,想要用在比較複雜的情況,它會讓你覺得礙眼 (eyesore),這時候你會想要用的是程序式巨集了。不過程序式巨集的缺點是它相對 heavyweight,因為程序式巨集要能夠直譯而不是語法分析 Rust token streams,你需要 synquote 和一些額外的編譯步驟,如果使用者用到你的程序式巨集,他們將花更多的時間在編譯上。

Invalid macro inputs

0:52:00

Q: Can you have a test where count is not a valid expression?
A: 可以的,請看以下例子:

macro_rules! avec { ($element:expr; $count:expr) => {{ let mut vs = Vec::new(); let x = $element; for _ in 0..$count { vs.push(x.clone()); } vs }}; } #[test] fn invalid_count() { let x: Vec<u32> = avec![42; "foo"]; }

可以通過編譯器檢查以及巨集可以成功展開:

$ cargo check
warning: unused macro definition: `avec`
 --> src/lib.rs:1:14
  |
1 | macro_rules! avec
  |              ^^^^
  |
  = note: `#[warn(unused_macros)]` on by default

warning: `vecmac` (lib) generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
$ cargo expand --tests --lib
...
fn invalid_count() {
    let x: Vec<u32> = {
        let mut vs = Vec::new();
        let x = 42;
        for _ in 0.."foo" {
            vs.push(x.clone());
        }
        vs
    };
}
...

當 Rust 在編譯由巨集產生的程式碼的時候,若遇到錯誤,Rust 不是告訴你巨集展開後的結果引發錯誤的位置 (for _ in 0.."foo" ),而是你最源頭在呼叫巨集的時候,是巨集的哪個輸入有問題,這對於我們除錯是非常有用的訊息 :

$ cargo test
   Compiling vecmac v0.1.0 (/home/wilson/CrustOfRust/vecmac)
warning: unused macro definition: `avec`
 --> src/lib.rs:1:14
  |
1 | macro_rules! avec
  |              ^^^^
  |
  = note: `#[warn(unused_macros)]` on by default

warning: `vecmac` (lib) generated 1 warning
error[E0308]: mismatched types
  --> src/lib.rs:15:33
   |
15 |     let x: Vec<u32> = avec![42; "foo"];
   | 

:pencil2: 前面的 clone_2_nonliteral 測試,編譯器給的錯誤訊息也是給巨集哪個輸入有問題的訊息:

$ cargo test ... ---- clone_2_nonliteral stdout ---- thread 'clone_2_nonliteral' panicked at 'called `Option::unwrap()` on a `None` value', src/lib.rs:24:38 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ...

Q: will a clone call on a literal, like 42.clone(), be optimized out by the compiler?
A: 是的,通常編譯器會對這個做最佳化。如果編譯器知道要 clone 的東西是 Copy 型態,編譯器就會將它最佳化成 copy,copy 只是暫存器與暫存器之間值的移動,操作成本比 clone 還要低。

Q: Could you use this newly introduced syntax to do say compact arbitrarily nested for loops?
A: Jon 沒跟到這個訊息。通常 marco_rules 在編寫上有相當的限制,雖然你可以編寫模式做到 match 一個 for 迴圈。

Test that something doesn't compile

0:54:52

接下來要講的是我們目前的方法的缺點:
Rust 沒有辦法說單元測試不應該編譯,不過有個 crate : compile_fail 會告訴rustdoc,這部份的程式碼應該要編譯失敗,藉此讓你邊寫不該編譯的測試:

/// ```compile_fail /// let x: Vec<u32> = vecmac::avec![42; "foo"]; /// ``` #[allow(dead_code)] struct CompileFailTest;

:bulb: 0:58:29
Q: trybuild is great for proc-macros as well
A: trybuild crate 也可以做 compile fail 測試。

Q: what about #[should_panic]?
A: 它仍可以編譯,它跟 compile fail 不同。

:warning: 注意到要在 documentation test 使用巨集的時候,是要用 vecmac::avec![42; "foo"],如果誤用成 avec![42; "foo"] 在本來就會通過無法編譯的程式碼看不出什麼端倪。但如果是要測試本來就會通過的 avec![42; 2] 竟然也可以通過編譯失敗的測試,而之所以失敗是因為找不到這個巨集, vecmac::avec![42; 2],才可以得到不通過編譯失敗的測試。

目前程式碼
#[macro_export] macro_rules! avec { () => { Vec::new() }; ($($element:expr),+ $(,)?) => {{ let mut vs = Vec::new(); $(vs.push($element);)+ vs }}; ($element:expr; $count:expr) => {{ let mut vs = Vec::new(); let x = $element; for _ in 0..$count { vs.push(x.clone()); } vs }}; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn single() { let x: Vec<u32> = avec![42]; assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); } #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); } #[test] fn trailing() { let _: Vec<&'static str> = avec![ "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", ]; } #[test] fn clone_2() { let x: Vec<u32> = avec![42; 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); } #[test] fn clone_2_nonliteral() { let mut y = Some(42); let x: Vec<u32> = avec![y.take().unwrap(); 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); } /// ```compile_fail /// let x: Vec<u32> = avec![42; "foo"]; /// ``` #[allow(dead_code)] struct CompileFailTest;

測試會出現以下的訊息,該訊息是執行 documentation test 的結果:

$ cargo test
...
running 1 test
test src/lib.rs - CompileFailTest (line 71) - compile fail ... ok
...

Tidying up the patterns

0:56:50

回到最一開始看到標準函式庫的 Vec 巨集:

macro_rules! vec { () => { ... }; ($elem:expr; $n:expr) => { ... }; ($($x:expr),+ $(,)?) => { ... }; }

其中第三個模式其實也可以支援 0 個元素,這樣就不需要 () => { ... }; 模式了:

#[macro_export] macro_rules! avec { ($($element:expr),* $(,)?) => {{ // #[allow(unused_mut)] let mut vs = Vec::new(); $(vs.push($element);)* // + 換成 * 以支援重複 0 次 vs }}; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); assert_eq!(x.len(), 0); }

雖然可以通過測試:

$ cargo test ... running 1 test test empty_vec ... ok ...

不過你會得到惱人的提示訊息:
image
為了不得到這個訊息,依照它的提示新增 #[allow(unused_mut)] 這一行即可。

Q: @jonhoo is #[macro_export] always required?
A: 不總是,如果巨集跟呼叫的地方在相同的 crate 就不用。

目前程式碼
#[macro_export] macro_rules! avec { ($($element:expr),* $(,)?) => {{ #[allow(unused_mut)] let mut vs = Vec::new(); $(vs.push($element);)* vs }}; ($element:expr; $count:expr) => {{ let mut vs = Vec::new(); let x = $element; for _ in 0..$count { vs.push(x.clone()); } vs }}; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn single() { let x: Vec<u32> = avec![42]; assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); } #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); } #[test] fn trailing() { let _: Vec<&'static str> = avec![ "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", ]; } #[test] fn clone_2() { let x: Vec<u32> = avec![42; 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); } #[test] fn clone_2_nonliteral() { let mut y = Some(42); let x: Vec<u32> = avec![y.take().unwrap(); 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); } /// ```compile_fail /// let x: Vec<u32> = avec![42; "foo"]; /// ``` #[allow(dead_code)] struct CompileFailTest;

Reallocations for repetition constructor

0:59:05

到目前為止我們的巨集仍有一些缺陷,舉例來說,假設你巨集給定 [1, 2, 3, 4, ..., 1024] 輸入,然後巨集展開後的程式碼會呼叫 1024 次 push,這將會對效能造成衝擊,因為 Vec 是動態的陣列,只要容量不夠用,就需要重新配置記憶體,並將資料從舊的 Vec 搬到新的 Vec。我們這裡想做的是,一開始就讓 Vec 有 1024 容量大小,就可以避免這個問題。

如果使用者提供了元素數量,這很容易實作:

#[macro_export] macro_rules! avec { ... ($element:expr; $count:expr) => {{ let count = $count; let mut vs = Vec::with_capacity(count); let x = $element; for _ in 0..count { vs.push(x.clone()); } vs }}; }

Line 6 避免 double evaluation
雖然我們擺脫了重新配置記憶體的問題,但 Line 10 仍有改進的空間,因為每次 push 都要做指標的增加。如果我們能夠在 for 迴圈創造 1024 個元素的 Vec,這樣就可以讓每道指令都很重要,因為不用每次在 push 時去檢查邊界和配置記憶體的問題。

我們可以用 extend 來避免 push 1024 次:

#[macro_export] macro_rules! avec { ... ($element:expr; $count:expr) => {{ let count = $count; let mut vs = Vec::with_capacity(count); - let x = $element; - for _ in 0..count { - vs.push(x.clone()); - } + vs.extend(std::iter::repeat($element).take(count)); vs }}; }

Line 12 使用方便的 std::iter::repeat 方法,只要你從迭代器中獲取,它就會產生您給出的元素的複製,而 take 方法是迭代器的一個方法,take 方法告訴迭代器只拿這麼多東西。extend 是當你在已經知道元素的數量的情況下,你可以先做邊界檢查,而不用在每次迭代都做邊界檢查。

:pencil2: std::iter::repeat 有一個 bound,即元素的類型必須實作 clone。

Q: Do you think the docs will be more readable with just that one pattern or with 2/3?
A: 模式越少越好,但如果為了讓一個模式支援很複雜的情況,建議還是多寫幾個模式比較好。

:question: 1:03:04
Q: default capacity is 10 on the first allocation i think
A: yeah that makes sense, the argument still holds

Q: Could the compiler optimize a series of pushes using a with_capacity call, or would that be incorrect?
A: 編譯器可以做到,但 Jon 認為編譯器會這麼做,因為會讓最佳化變得更加複雜,因為想做到這件事,必須讓編譯器知道 semantics effect,它需要知道 newpush 以及 with_capacity 之間有一些關係,這會導致編譯時間過長。:question:1:03:54 如果我們使用 Vec::from_iter 並成功生產實作確切大小迭代器的迭代器,編譯器將執行這個操作,但這不是今天要探討的主題。

Macro argument trait bounds

1:04:08

cheezgi: no, the compiler will check that x implements the Clone trait
Q: @cheezgi Are you sure? Because Clone is never actually mentioned, and I thought macros were really just substitution.
A: 巨集並沒有 trait bounds,巨集並沒有說模式內的型別需要實作 clone,巨集通常不能表達這個,取而代之的是,如果你嘗試使用非 clone 型態的類別,編譯器仍可以成功展開巨集,也可以通過編譯器檢查,但要測試時,編譯器會抱怨 Line 8 的 $element 沒有實作 clone trait,但錯誤的訊息一樣會 bubble up ,會告訴你模式的哪個變數有問題:

#[macro_export] macro_rules! avec { ($element:expr; $count:expr) => {{ let count = $count; let mut vs = Vec::with_capacity(count); vs.extend(std::iter::repeat($element).take(count)); vs }}; } #[test] fn non_lone() { struct Foo; let y = Foo; let x: Vec<Foo> = avec![Foo; 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], Foo); assert_eq!(x[1], Foo); }

只節錄部份的測試的錯誤訊息,更多有用的錯誤訊息請嘗試自行測試並閱讀:

$ cargo test Compiling vecmac v0.1.0 (/home/wilson/CrustOfRust/vecmac) error[E0277]: the trait bound `Foo: Clone` is not satisfied --> src/lib.rs:17:29 | 7 | vs.extend(std::iter::repeat($element).take(count)); | ----------------- required by a bound introduced by this call ... 17 | let x: Vec<Foo> = avec![Foo; 2]; | ^^^ the trait `Clone` is not implemented for `Foo` | ...

Q: Jon, just checked and the capacity from plain pushing goes 0, 1, 2, 4, 8, etc, so it's actually a little worse from just picking 16
A: 是的,2 的冪會成長一次,所以前面舉的 1024 個元素的例子,會經歷 10 次重新配置記憶體的情形。

"use" hygiene in macros

1:06:40

Q: jonhoo is that hygienic? What happens if the caller defined a mod std mod iter with repeat in it?
A: 這是你必須要小心的,因為以下部份並不是巨集 hygiene 的處理範圍:

#[macro_export] macro_rules! avec { ($element:expr; $count:expr) => {{ ... // 這裡你能可遇到 caller 的模組蓋掉你的 std 模組, // std 可能引用 caller scope 內的一個模組,該模組不是標準函式庫。 vs.extend(std::iter::repeat($element).take(count)); // 避免上述問題的方法有兩種寫法: // 1. 使用 $crate:: prefix // 這樣巨集就只會參照到定義它的那個 crate 所使用到的 crate, // 就不會去擔心 caller 有可能會覆寫掉你不想別人覆寫的那行。 // vs.extend($crate::std::iter::repeat($element).take(count)); // 2. 使用 :: prefix // root level 路徑,所以 std 必須是個 crate, 而不是一個模組。 // 但 caller 若使用重新命名 crate std,caller 的程式將會問到問題。 // vs.extend(::std::iter::repeat($element).take(count)); ... }}; }

Q: why not Vec::resize?
A: Vec::resize 也可以,以下的程式碼也可以通過測試:

#[macro_export] macro_rules! avec { ($element:expr; $count:expr) => {{ let count = $count; let mut vs = Vec::with_capacity(count); vs.resize(count, $element); vs }}; } #[test] fn clone_2() { let x: Vec<u32> = avec![42; 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); }

resize 的方法或許更有效率,因為 resize 不需要做邊界檢查。

The standard library pattern

1:08:28

Q: the reason why standard library deals with trailing comma as it does because it disallows invocation with comma only like so "vec![,]"
A: 我們的程式碼是可以允許 avec![,] 的,因為我們的模式是 ($($element:expr),* $(,)?),而 Vec 標準函式庫用的是 ($($element:expr),+ $(,)?)

接著展示巨集用遞迴呼叫來解決尾隨逗號的情形:

#[macro_export] macro_rules! avec { ($($element:expr),*) => {{ #[allow(unused_mut)] let mut vs = Vec::new(); $(vs.push($element);)* vs }}; // 只要輸入有尾隨逗號,就會呼叫第二個模式 ($($element:expr,)*) => {{ // 展開後會變成 [x1, x2, x3, x4,] $crate::avec![$($element),*] }}; ($element:expr; $count:expr) => {{ let count = $count; let mut vs = Vec::with_capacity(count); vs.resize(count, $element); vs }}; } // 現在只要一個 test case 來測我們要實作的功能 #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); }

注意到,第一個模式與第二個模式的順序不能相反,否則若輸入是 avec[] 時會導致無窮迴圈,因為第一個模式放前面的話,一開始就比中就不會有遞迴呼叫的問題了。

目前程式碼 (這個版本不支援 avec![,])
#[macro_export] macro_rules! avec { ($($element:expr),*) => {{ #[allow(unused_mut)] let mut vs = Vec::new(); $(vs.push($element);)* vs }}; ($($element:expr,)*) => {{ $crate::avec![$($element),*] }}; ($element:expr; $count:expr) => {{ let count = $count; let mut vs = Vec::with_capacity(count); vs.resize(count, $element); vs }}; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn single() { let x: Vec<u32> = avec![42]; assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); } #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); } #[test] fn trailing() { let _: Vec<&'static str> = avec![ "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", ]; } #[test] fn clone_2() { let x: Vec<u32> = avec![42; 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); } #[test] fn clone_2_nonliteral() { let mut y = Some(42); let x: Vec<u32> = avec![y.take().unwrap(); 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); } /// ```compile_fail /// let x: Vec<u32> = avec![42; "foo"]; /// ``` #[allow(dead_code)] struct CompileFailTest;

The need for counting

1:10:20

先改進巨集的第三個模式:

#[macro_export] macro_rules! avec { ($($element:expr),*) => {{ #[allow(unused_mut)] let mut vs = Vec::new(); $(vs.push($element);)* vs }}; ($($element:expr,)*) => {{ $crate::avec![$($element),*] }}; ($element:expr; $count:expr) => {{ // 因為 count 現在只要用一次, // 所以不需要指派到暫存變數避免 double evaluation // let count = $count; // let mut vs = Vec::with_capacity(count); let mut vs = Vec::new(); vs.resize($count, x); vs }}; }

第一個模式沒有 count 的輸入,要如何使用 with_capactiy 呢? 有很多方法可以數有幾個輸入元素,詳細 counting 的技巧請參照 : The Little Book of Rust Macros : Counting,本文只示範其中一種技巧。

Eager macro errors

1:11:38

Q: The Clone question was still from the earlier version without std::iter. Would it be possible to add a bound there?
A: 你可以這樣做:

#[macro_export] macro_rules! avec { ... ($element:expr; $count:expr) => {{ let x = $element; // 在這裡檢查 bound fn test<C: Clone>(_: &C) {} test(&x); let mut vs = Vec::new(); vs.resize($count, x); vs }}; }

當您編寫巨集時,您只需按照編寫程式碼的方式編寫它們即可,通常在執行 cargo test 的時候,編譯器就可以檢查到要求要實作 clone 的地方,卻不是給它 clone 的變數,它就會告訴你模式內的哪個輸入有問題了。另外,如果有些東西不是很明顯的,你想要加在文件說明,你不能在巨集內部對某個模式撰寫文件,你只能在巨集的最外部撰寫文件,也就是在 #[macro_export] 之前加上 /// 或是 marco_rules! avec 之前加上 ///

Q: Can you access data from the calling scope from within the macro?
A: 不行,除非你用到前面的方法,也就是將變數傳入巨集內部才可以。

Counting in macros

1:13:00

目前程式碼
#[macro_export] macro_rules! avec { ($($element:expr),*) => {{ #[allow(unused_mut)] let mut vs = Vec::new(); $(vs.push($element);)* vs }}; ($($element:expr,)*) => {{ $crate::avec![$($element),*] }}; ($element:expr; $count:expr) => {{ let mut vs = Vec::new(); vs.resize($count, $element); vs }}; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn single() { let x: Vec<u32> = avec![42]; assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); } #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); } #[test] fn trailing() { let _: Vec<&'static str> = avec![ "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", ]; } #[test] fn clone_2() { let x: Vec<u32> = avec![42; 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); } #[test] fn clone_2_nonliteral() { let mut y = Some(42); let x: Vec<u32> = avec![y.take().unwrap(); 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); } /// ```compile_fail /// let x: Vec<u32> = avec![42; "foo"]; /// ``` #[allow(dead_code)] struct CompileFailTest;

Q: Could you have a version of the macro that defaults to the "evaluate element many times" if element doesn't implement Clone?
A: 可以,不過通常這不會是你想要的,clone 還是比較好。如果你可以重複執行 (reevaluate) 表達式很多次,為什麼你回傳的東西沒有實作 clone?

先定好要修改的範圍來支援巨集:

#[macro_export] macro_rules! avec { ($($element:expr),*) => {{ + let count = todo!(); // todo()! 部份必須是用巨集的功能來完成實作 #[allow(unused_mut)] - let mut vs = Vec::new(); + let mut vs = Vec::with_capacity(count); $(vs.push($element);)* vs }}; ... } ...

接著用愚蠢版本的方法來實作 count :

#[macro_export] macro_rules! avec { ($($element:expr),*) => {{ - let count = todo!(); + let count = [$($element),*].len(); + // 看似可以正常運作,但其實會面臨到兩個問題 + // 1. double evaluation + // 2. cannot infer type #[allow(unused_mut)] let mut vs = Vec::with_capacity(count); $(vs.push($element);)* vs }}; ... } ...
目前程式碼
#[macro_export] macro_rules! avec { ($($element:expr),*) => {{ let count = [$($element),*].len(); #[allow(unused_mut)] let mut vs = Vec::with_capacity(count); $(vs.push($element);)* vs }}; ($($element:expr,)*) => {{ $crate::avec![$($element),*] }}; ($element:expr; $count:expr) => {{ let mut vs = Vec::new(); vs.resize($count, $element); vs }}; } // 現在只要兩個 test case 來測我們要實作的功能 #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); }

聰穎版本的方法來實作 count,在計數的時候不要真的數元素 :

#[macro_export] macro_rules! avec { ($($element:expr),*) => {{ - let count = [$($element),*].len(); #[allow(unused_mut)] - let mut vs = Vec::with_capacity(count); + let mut vs = Vec::with_capacity($crate::avec![@COUNT; $($element),*]); + // 遞迴呼叫巨集來計數 $(vs.push($element);)* vs }}; ... + // @COUNT : 供巨集內部使用 + (@COUNT; $($element:expr),*) => { + // [$($element),*].len(); 這樣還是沒解決前面講的兩個問題 + [$(()), *].len() + } } ...
目前程式碼
#[macro_export] macro_rules! avec { ($($element:expr),*) => {{ #[allow(unused_mut)] let mut vs = Vec::with_capacity($crate::avec![@COUNT; $($element),*]); $(vs.push($element);)* vs }}; ($($element:expr,)*) => {{ $crate::avec![$($element),*] }}; ($element:expr; $count:expr) => {{ let mut vs = Vec::new(); vs.resize($count, $element); vs }}; // @COUNT : 供巨集內部使用 (@COUNT; $($element:expr),*) => { // [$($element),*].len(); 這樣還是沒解決前面講的兩個問題 [$(()), *].len() } } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); }

測試時還是遇到錯誤,這錯誤的意思是說 $() 裡面要變數名稱,Rust 才知道到底要重複幾次:

$ cargo test Compiling vecmac v0.1.0 (/home/wilson/CrustOfRust/vecmac) error: attempted to repeat an expression containing no syntax variables matched as repeating at this depth --> src/lib.rs:22:11 | 22 | [$(()), *].len() |

繼續改進程式碼:

#[macro_export] macro_rules! avec { ($($element:expr),*) => {{ #[allow(unused_mut)] let mut vs = Vec::with_capacity($crate::avec![@COUNT; $($element),*]); $(vs.push($element);)* vs }}; ... (@COUNT; $($element:expr),*) => { // 這次 Rust 知道要重複幾次了 // 這樣子一樣會出現 cannot infer type ↓↓↓ // [$($crate::avec![@SUBST; $element]),*].len() // 1:21:34 // 參數是陣列的參考,呼叫 len 的實作 for slices of unit // 注意到,是 slice 而不是陣列。 // 因為陣列實作了 AsRef trait,我們允許呼叫 slice 存在的任何方法 // 最終這個呼叫了 slice::len() 的方法,而參數是陣列的 slice <[()]>::len(&[$($crate::avec![@SUBST; $element]),*]) // 展開結果:<[()]>::len(&[(), ()]) }; // 第二個輸入提供表達式,不提供重複 (@SUBST; $_element:expr) => { // 回傳 Unit,這樣在數元素數量的時候,只要數 Unit 數量, // 這樣就解決 double evaluation 的問題了。 () // Unit type 不佔用任何記憶體空間 }; } ...
目前程式碼
#[macro_export] macro_rules! avec { ($($element:expr),*) => {{ #[allow(unused_mut)] let mut vs = Vec::with_capacity($crate::avec![@COUNT; $($element),*]); $(vs.push($element);)* vs }}; ($($element:expr,)*) => {{ $crate::avec![$($element),*] }}; ($element:expr; $count:expr) => {{ let mut vs = Vec::new(); vs.resize($count, $element); vs }}; (@COUNT; $($element:expr),*) => { <[()]>::len(&[$($crate::avec![@SUBST; $element]),*]) }; (@SUBST; $_element:expr) => { () }; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); }

Other ways of counting

1:24:48

更多有趣的計數方法請參照:The Little Book of Rust Macros : Counting

Q: Is there not some predefined count! macro or something like that? Seems like this would be needed pretty often.
A: 沒有,因為編譯器不知道你要計數的是什麼,有可能是 token 數量,有可能是語法樹中的字元數量,或是元素的數量。這樣的話你需要一個泛化巨集來告知說要計數的東西是什麼型別。

到這裡,巨集變得很複雜,這時候你會想要用的是程序式巨集了,不過本次實作到那麼複雜的原因是因為想讓你更加了解巨集是如何運作的,雖然你可能不一定非得自己用這種方式來實作巨集。

Ensuring count is computed at compile time

1:27:27

Q: Is there some way to test / verify that a length is known at compile time?
A: 請看下面範例:

#[macro_export] macro_rules! avec { ($($element:expr),*) => {{ + const _: usize = $crate::avec![@COUNT; $($element),*]; + #[allow(unused_mut)] let mut vs = Vec::with_capacity($crate::avec![@COUNT; $($element),*]); $(vs.push($element);)* vs }}; ... }

Q: Why not use int's like zeros for vec capacity?
A: 你不會想用 0,因為會一直重新配置記憶體。

Hiding internal macro patterns

1:28:32

Q: Do the @COUNT and @SUBST patterns show up in the docs and is there a way to hide them?
A: 很不幸的是,它們會出現在文件。解決辦法是將它們移出巨集並加上 doc(hidden) 的屬性:

#[macro_export] macro_rules! avec { ($($element:expr),*) => {{ - const _: usize = $crate::avec![@COUNT; $($element),*]; + const C: usize = $crate::count![@COUNT; $($element),*]; + // _ -> C : 值直接給 with_capacity 用 #[allow(unused_mut)] - let mut vs = Vec::with_capacity($crate::avec![@COUNT; $($element),*]); + let mut vs = Vec::with_capacity(C); $(vs.push($element);)* vs }}; ($($element:expr,)*) => {{ $crate::avec![$($element),*] }}; ($element:expr; $count:expr) => {{ let mut vs = Vec::new(); vs.resize($count, $element); vs }}; - (@COUNT; $($element:expr),*) => { - <[()]>::len(&[$($crate::avec![@SUBST; $element]),*]) - }; - (@SUBST; $_element:expr) => { - () - }; } +#[macro_export] +#[doc(hidden)] +macro_rules! count +{ + (@COUNT; $($element:expr),*) => { + // avec! -> count! + <[()]>::len(&[$($crate::count![@SUBST; $element]),*]) + }; + (@SUBST; $_element:expr) => { + () + }; +} ...

Q: Why does it matter if we use () when the length is known at compile time?
A: 如果我們沒有使用 unity type, Line 40 會有配置堆疊記憶體的額外開銷。或許它會被 LLVM 給最佳化掉,但如果我們用 unit type,可以保證說,完全不會有配置堆疊記憶體的額外開銷。

目前程式碼
#[macro_export] macro_rules! avec { ($($element:expr),*) => {{ const C: usize = $crate::count![@COUNT; $($element),*]; #[allow(unused_mut)] let mut vs = Vec::with_capacity(C); $(vs.push($element);)* vs }}; ($($element:expr,)*) => {{ $crate::avec![$($element),*] }}; ($element:expr; $count:expr) => {{ let mut vs = Vec::new(); vs.resize($count, $element); vs }}; } #[macro_export] #[doc(hidden)] macro_rules! count { (@COUNT; $($element:expr),*) => { <[()]>::len(&[$($crate::count![@SUBST; $element]),*]) }; (@SUBST; $_element:expr) => { () }; } #[test] fn empty_vec() { let x: Vec<u32> = avec![]; assert!(x.is_empty()); } #[test] fn single() { let x: Vec<u32> = avec![42]; assert!(!x.is_empty()); assert_eq!(x.len(), 1); assert_eq!(x[0], 42); } #[test] fn double() { let x: Vec<u32> = avec![42, 43]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 43); } #[test] fn trailing() { let _: Vec<&'static str> = avec![ "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", "uaghdyuaisdqowiejiqthsdfhsdjkqkjahfiuqwdhquwhdqiowda", ]; } #[test] fn clone_2() { let x: Vec<u32> = avec![42; 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); } #[test] fn clone_2_nonliteral() { let mut y = Some(42); let x: Vec<u32> = avec![y.take().unwrap(); 2]; assert!(!x.is_empty()); assert_eq!(x.len(), 2); assert_eq!(x[0], 42); assert_eq!(x[1], 42); } /// ```compile_fail /// let x: Vec<u32> = avec![42; "foo"]; /// ``` #[allow(dead_code)] struct CompileFailTest;

Other collection literals

1:31:13

本次實作 Vec 的巨集,你也可以想像用巨集實作 hash map, btree map, set,只是現在的元素換成 key->value,然後呼叫 insert 的方法,而不是 push 方法。有一個 crate : maplit,就是在做這件事。Jon 強烈建議你可以自己練習,練習素材就是以本次實作為基礎去做擴展功能,或者是改實作別的資料結構。

Comparing against the std implementation

1:33:00

標準函式庫的部份程式碼:

#[cfg(all(not(no_global_oom_handling), test))] #[allow(unused_macro_rules)] macro_rules! vec { () => ( $crate::vec::Vec::new() ); ($elem:expr; $n:expr) => ( $crate::vec::from_elem($elem, $n) ); ($($x:expr),*) => ( // 這版本的實作比我們的簡單的非常多 <3 $crate::slice::into_vec($crate::boxed::Box::new([$($x),*])) // 1:33:21 // [$($x),*]:創建有所有元素的陣列 // boxed::Box::new() 告訴編譯器說,把那個陣列創建在 heap 上 // 再把 slice 透過 into_vec 轉成 Vec。 // 當你在 heap 上有一系列的東西,像是你有那個指標, // 你可以很容易的創造出 Vec ); ($($x:expr,)*) => (vec![$($x),*]) }

待整理

  1. 0:06:18
  2. 0:23:48
  3. 0:28:22
  4. 1:03:04
  5. 1:21:34 <[()]>
  6. 1:33:21 $crate::slice::into_vec($crate::boxed::Box::new([$($x),*]))