Daily Archives: 19/01/2018

Rust – Macro

Trong các bài trước chúng ta đã làm việc rất nhiều với macro. Mỗi lần chúng ta gọi một hàm mà có dấu chấm than ! ở sau tên thì đó chính là một macro có sẵn trong Rust. Những macro mà chúng ta đã sử dụng là println!, panic!, vec!, assert!… Kể từ bài này mình sẽ gọi chúng ta macro chứ không gọi là hàm như những bài trước nữa.

Tại sao cần đến macro?

Macro cho phép chúng ta tạo nên các cú pháp gọn nhẹ nhưng mạnh mẽ, nhờ đó mà việc lập trình trở nên dễ dàng hơn rất nhiều. Chẳng hạn như trong Rust có macro regex! cho phép chúng ta định nghĩa các biểu thức chính quy, các biểu thức chính quy này sẽ được dịch và sử dụng.

Macro giúp giảm đi việc lặp lại code nhiều lần, chúng ta có thể định nghĩa một macro thực hiện một công việc nào đó và gọi mỗi khi cần.

Giống như với các ngôn ngữ khác, chúng ta cũng có thể định nghĩa macro của riêng chúng ta trong Rust.

Cú pháp

Chúng ta định nghĩa một macro theo như mẫu dưới đây:

macro_rules! mac1 {
(pattern) => (expansion);
(pattern) => (expansion);

}

Đầu tiên chúng ta ghi từ khóa macro_rules!, sau đó là tên macro, trong đoạn code trên thì tên macro là mac1, rồi đến cặp dấu {}, và danh sách các quy luật, mỗi luật kết thúc bằng dấu chấm phẩy ;

Quy luật mà chúng ta nói tới ở đây có thể hiểu như trong câu lệnh match mà chúng ta đã học (hay câu lệnh switch của C++, Java…), tức là chúng ta có thể truyền vào các tham số, và Rust sẽ so sánh xem tham số nào phù hợp và chạy đoạn code phía sau đó. Ví dụ:

fn main() {
macro_rules! hello {
() => {
println!("Hello World");
}
}
hello!();
}

Trong đoạn code trên có một macro tên là hello, bên trong chúng ta định nghĩa một luật, luật này không nhận vào tham số nào. Khi chúng ta gọi macro này thì chúng ta ghi là hello!() và luật đầu tiên khớp sẽ chạy, ở đây chuỗi Hello World sẽ được in ra.

Hello World

Khi muốn đưa tham số vào thì chúng ta đưa với cấu trúc sau:

$arg:frag

Trong đó $arg chỉ là tên biến bình thường như khi định nghĩa hàm, đối với tham số cho macro thì phải có dấu $. Còn frag có thể là một trong những giá trị sau:

  • expr: biểu thức cho ra một giá trị (vd 2+2, ‘phocode’)
  • item: một thành phần của một crate (chẳng hạn struct Blog)
  • block: một khối lệnh (vd { println!(); return 12; } )
  • stmt: một câu lệnh (vd let x = 3)
  • pat: mẫu hay patter (vd (17, 'a') )
  • ty: biến theo sau bởi dấu => hoặc : => as
  • ident: tên một biến (vd x; str)
  • path: tên một struct, interface… hợp lệ (vd Blog::PhoCode)
  • tt: token tree (bạn có thể tự tìm hiểu thêm)

Ví dụ:

fn main() {
macro_rules! hello_name {
($arg:expr) => {
println!("Hello {}", $arg);
}
}
hello_name!("Pho Code");
}

Trong đoạn code trên, macro hello_name nhận vào một luật là nhận một biến kiểu expr, tức là chúng ta có thể truyền vào một biểu thức hay một giá trị. Bên trong chúng ta cho in giá trị của biểu thức đó ra.

Hello Pho Code

Macro nhiều tham số

Chúng ta có thể truyền vào nhiều tham số cho một macro, ví dụ:

fn main() {
macro_rules! arguments_list {
(
$($arg:expr), *) => (
{$
( println!("{}", $arg); );
*}
);
}
arguments_list!("Pho Code", 2017, 3.14);
}

Cú pháp này của Rust hơi lạ mắt và khó đọc, bạn có thể tự suy ra, ở đây mình sẽ không giải thích. Bạn có thể viết liền trên một hàng chứ không nhất thiết phải xuống dòng như mình.

Pho Code
2017
3.14

Tạo hàm mới

Chúng ta có thể dùng macro để tạo một hàm mới như sau:

fn main() {
macro_rules! create_a_function {
($arg:ident) => {
fn $arg() {
println!("Calling the {} function", stringify!($arg));
}
}
}
create_a_function!(phocode);
phocode();
}

Trong đoạn code trên, chúng ta tạo một macro có tên create_a_function, macro này nhận vào tên hàm, bên trong chúng ta định nghĩa một hàm mới với tên lấy từ $arg. Khi gọi create_a_function!() là chúng ta đã tạo được một hàm mới và có thể gọi một cách bình thường.

Ngoài ra ở đây chúng ta còn dùng đến macro stringify để chuyển $arg thành một chuỗi.

Calling the phocode function

Gọi macro của crate khác

Đây là trường hợp đã được đề cập trong bài trước, khi chúng ta gọi macro error!() của crate log, chúng ta phải thêm dòng #[macro_use] vào đầu file.

#[macro_use]
extern crate log;
extern crate env_logger;

fn main() {
env_logger::init();
error!("There are some errors");
}

Trong trường hợp chúng ta chỉ muốn sử dụng một số macro nào đó của crate thì chúng ta có thể viết như sau:

#[macro_use(error, info)] // Chỉ sử dụng macro errror!() và info!()
extern crate log;
extern c<span data-mce-type="bookmark" style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" class="mce_SELRES_start"></span>rate env_logger;

fn main() {
env_logger::init();
error!("There are some errors");
}

Nếu không thì chúng ta sẽ không thể sử dụng được macro nào của crate đó. Hoặc có một cách tiếp cận khác là trong crate đó chúng ta khai báo #[macro_export] vào đầu file, và chúng ta sẽ có thể sử dụng macro trong file khác mà không cần khai báo lại #[macro_use] nữa.