Phiên bản beta Move 2024 hiện bao gồm
vĩ mô
chức năng!
Các hàm macro trong Move tương tự như các hàm thông thường: bạn định nghĩa các tham số, kiểu trả về và logic xác định cách các hàm macro của bạn hoạt động, sau đó bạn có thể gọi các hàm macro của mình từ bất kỳ đâu trong mã của mình. Tuy nhiên, không giống như các hàm thông thường, trình biên dịch mở rộng các hàm đó tại chỗ để thực hiện các hoạt động được xác định trực tuyến.
Sự khác biệt chính giữa các hàm macro và các hàm bình thường là:
Mở rộng cú pháp hàm macro
Di chuyển trình biên dịch mở rộng và gọi ngữ nghĩa
Tham số Lambda
Mở rộng cú pháp: Cú pháp di chuyển chỉ định cách xác định hàm macro, đối số của chúng và cách gọi chúng. Các quy tắc này không chỉ giúp trình biên dịch phân biệt macro với hàm bình thường mà còn cung cấp sự phân biệt trực quan giữa hàm bình thường và hàm macro trong quá trình sử dụng.
Mở rộng trình biên dịch và ngữ nghĩa gọi: Move eager đánh giá các đối số hàm bình thường. Nói cách khác, runtime đánh giá đầy đủ các đối số bạn truyền cho một hàm bình thường, bất kể có cần thiết cho logic hàm hay không. Ngược lại, các hàm macro trực tiếp thay thế các biểu thức cho các đối số của chúng trong quá trình mở rộng và runtime sẽ không thấy lệnh gọi macro chút nào! Điều này có nghĩa là các đối số không được đánh giá chút nào nếu mã thân macro có đối số không bao giờ được đạt tới. Ví dụ, nếu đối số được thay thế trong một biểu thức if-else và nhánh có đối số đó không bao giờ được lấy, thì đối số sẽ không được đánh giá.
Tham số lambda: Ngoài các biểu thức bình thường, các hàm Macro cho phép bạn cung cấp mã tham số hóa dưới dạng đối số hàm bậc cao. Điều này có thể thực hiện được nhờ sự ra đời của kiểu lambda, cho phép các tác giả macro tạo ra các macro mạnh mẽ và linh hoạt.
Để biết thêm chi tiết về cách xác định hàm macro, hãy xem tài liệu tham khảo chính thức trong sách Move.
Giải phẫu các hàm vĩ mô
Trình biên dịch Move mở rộng các hàm macro trong quá trình biên dịch, cho phép khả năng biểu đạt không có trong các hàm thông thường. Để chỉ ra hành vi thay thế đặc biệt này, các hàm macro có các tham số kiểu và tham số biểu thức được thêm tiền tố là
$
ký tự. Hãy xem xét một hàm Move có tên `my_assert` hoạt động giống như hàm nguyên thủy assert! của trình biên dịch (không có lỗi thông minh).
macro fun my_assert($cond: bool, $code: u64) { nếu (!$cond) hủy bỏ $code. }
Trình biên dịch Move thay thế các đối số thành các tham số kiểu và tham số biểu thức trong quá trình mở rộng. Hành vi này tạo ra quá trình đánh giá lười biếng cho các đối số được đề cập trong phần trước. Như mã cho thấy, macro `my_assert` kiểm tra điều kiện được truyền cho nó, hủy bỏ chương trình nếu điều kiện `$cond` giải quyết thành
SAI
(hoặc không `true`) với `$code` được truyền dưới dạng đối số thứ hai.
Để triệu hồi
khẳng định của tôi
hàm macro, lập trình viên sử dụng ký tự ! sau tên hàm để chỉ ra trực quan rằng các đối số không được đánh giá một cách háo hức:
my_assert!(vector[] == vector[], 0); my_assert!(true, 1 / 0); // sẽ không hủy bỏ! '1 / 0' không được đánh giá.
Như bạn có thể nhận thấy,
$mã
giá trị được truyền cho macro trong lần gọi thứ hai bao gồm phép chia cho số không. Mặc dù có chút gượng ép, mục đích của phép chia này là để chỉ ra rằng trình biên dịch sẽ không đánh giá biểu thức vì luồng điều khiển sẽ không bao giờ đạt đến quá trình thực thi của nó do điều kiện đúng được cung cấp.
Macro với đối số lambda
Việc bổ sung các hàm macro bao gồm việc giới thiệu một tính năng hoàn toàn mới: lambda. Sử dụng các tham số với các kiểu lambda, bạn có thể truyền mã từ người gọi vào phần thân của macro. Mặc dù việc thay thế được thực hiện tại thời điểm biên dịch, chúng được sử dụng tương tự như các hàm ẩn danh, lambda hoặc closure trong các ngôn ngữ khác.
Ví dụ, hãy xem xét một vòng lặp thực hiện lambda trên mỗi số từ
0
đến số n mà mã cung cấp.
macro công khai fun do($n: u64, $f: |u64| -> ()) { let mut i = 0; let stop = $n; while (i < stop) { $f(i); i = i + 1; } }
Tham số lambda (
$f
) là đối số thứ hai của macro do. Kiểu lambda được định nghĩa là |u64| -> (), lấy u64 làm đầu vào và trả về (). Kiểu trả về của () là ngầm định, do đó đối số cũng có thể được viết đơn giản là $f: |u64|.
Logic chương trình của bạn sau đó có thể sử dụng macro này để tính tổng các số từ
0
đến 10 bằng cách sử dụng tham số lambda:
cho tổng mut = 0; thực hiện!(10, |i| tổng = tổng + i);
Ở đây, đoạn mã `|i| sum = sum + i` định nghĩa lambda cho lệnh gọi macro. Khi nó được gọi trong phần thân của macro, các đối số của nó được đánh giá và ràng buộc trong phần thân của lambda. Lưu ý rằng, không giống như lệnh gọi macro, lambda sẽ đánh giá hoàn toàn các đối số của chúng trước khi thực thi phần thân lambda.
Các hàm macro là hợp vệ sinh, do đó i trong lambda không giống với i trong do. Xem tài liệu tham khảo Move để biết thêm chi tiết.)
Macro trong thư viện chuẩn Move
Với việc bổ sung các macro,
di chuyển stdlib
Thư viện giới thiệu một số hàm macro để nắm bắt các mẫu lập trình phổ biến.
Macro số nguyên
Vì
u64
và các kiểu số nguyên khác, thư viện move-stdlib cung cấp một tập hợp các hàm macro giúp đơn giản hóa các vòng lặp thông thường. Bài đăng này sẽ xem xét kiểu u64, nhưng các hàm macro tương tự cũng khả dụng cho u8, u16, u32, u128 và u256.
Bạn đã thấy một ví dụ, vì mã hàm do là một hàm macro trong std::u64.
Danh sách đầy đủ các hàm macro hiện có cho kiểu số nguyên là:
macro công khai vui vẻ làm ($stop: u64, $f: |u64|)
Vòng lặp áp dụng $f cho mỗi số từ 0 đến $stop (không bao gồm).
macro công khai fun do_eq($stop: u64, $f: |u64|)
Vòng lặp áp dụng $f cho mỗi số từ 0 đến $stop (bao gồm).
macro công khai fun range_do($start: u64, $stop: u64, $f: |u64|)
Vòng lặp áp dụng $f cho mỗi số từ $start đến $stop (không bao gồm).
macro công khai fun range_do_eq($start: u64, $stop: u64, $f: |u64|)
Vòng lặp áp dụng $f cho mỗi số từ $start đến $stop (bao gồm cả $f và $f).
Để có ví dụ đơn giản hơn về việc áp dụng các hàm macro đã xác định, hãy xem xét vòng lặp sau:
hãy để mut i = 0; trong khi (i < 10) { foo(i); i = i + 1; }
Bạn có thể viết lại vòng lặp này bằng cách triển khai
LÀM
hàm macro cho std::u64 như sau:
10u64.do!(|i| foo(i));
Tương tự như vậy, bạn có thể làm cho vòng lặp sau ngắn gọn hơn bằng cách sử dụng
hàm macro u64::range_do_eq
:
let mut i = 1; // lặp từ 1 đến 10^8 trong 10 giây while (i <= 100_000_000) { foo(i); i = i * 10; }
Kết quả của việc viết lại:
1u64.range_do_eq!(8, |i| foo(10u64.pow(i)));
Mặc dù cách này có thể dễ đọc hơn, nhưng sẽ tốn nhiều công sức hơn để thực hiện.
vectơ
vĩ mô
Trong một tinh thần tương tự như số nguyên
LÀM
macro, bạn có thể lặp lại các phần tử của một vector bằng cách sử dụng hàm do.
vector[1, 2, 3, 4, 5].làm!(|x| foo(x));
Điều này tương đương với:
hãy để mut v = vector[1, 2, 3, 4, 5]; v.reverse(); trong khi (!v.is_empty()) { foo(v.pop_back()); }
Mã mở rộng cho thấy quá trình này sử dụng vectơ. Sử dụng
do_ref
và các macro do_mut để lặp lại theo tham chiếu hoặc theo tham chiếu có thể thay đổi.
fun check_coins(coins: &vector<Coin<SUI>>) { coins.do_ref!(|coin| assert!(coin.value() > 0)); /* mở rộng thành let mut i = 0; let n = coins.len(); while (i < n) { let coin = &coins[i]; assert!(coin.value() > 0); i = i + 1; } */ } fun take_10(coins: &mut vector<Coin<SUI>>) { coins.do_mut!(|coin| transfer::public_transfer(coin.take(10), @0x42)); /* mở rộng thành let mut i = 0; let n = coins.len(); while (i < n) { let coin = &mut coins[i]; transfer::public_transfer(coin.take(10), @0x42); i = i + 1; } */ }
Ngoài việc lặp lại, bạn có thể sửa đổi và tạo các vector bằng macro.
vector::bảng
tạo một vectơ có độ dài n bằng cách áp dụng lambda cho mỗi chỉ mục.
fun powers_of_2(n: u64): vector<u64> { vector::tabulate(n, |i| 2u64.pow(i)) /* mở rộng thành let mut v = vector[]; let mut i = 0; while (i < n) { v.push_back(2u64.pow(i)); i = i + 1; }; v */ }
vector::bản đồ
tạo một vector mới từ một vector hiện có bằng cách áp dụng lambda cho mỗi phần tử. Trong khi vector::map hoạt động trên một vector theo giá trị, các macro map_ref và map_mut hoạt động trên một vector theo tham chiếu và tham chiếu có thể thay đổi.
vui vào_cân_bằng(tiền xu: vector<Coin<SUI>>): vector<Balance<SUI>> { coins.map!(|coin| coin.into_balance()) /* mở rộng thành let mut v = vector[]; coins.reverse(); while (!coins.is_empty()) { let coin = coins.pop_back(); v.push_back(coin.into_balance()); }; v */ }
Tương tự như bản đồ,
vector::lọc
tạo một vectơ mới từ một vectơ hiện có bằng cách chỉ giữ lại các phần tử thỏa mãn một vị ngữ.
fun non_zero_numbers(numbers: vector<u64>): vector<u64> { numbers.filter!(|n| n > 0) /* mở rộng thành let mut v = vector[]; numbers.reverse(); while (!numbers.is_empty()) { let n = numbers.pop_back(); if (n > 0) { v.push_back(n); } }; v */ }
Xem Bảng A ở cuối bài viết này để biết danh sách các hàm macro hiện có cho vectơ.
lựa chọn
vĩ mô
Trong khi
Lựa chọn
không phải là một tập hợp có nhiều hơn một phần tử, kiểu này có các macro do (và do_ref và do_mut) để dễ dàng truy cập giá trị của nó nếu nó là một số.
fun maybe_transfer(coin: Option<Coin<SUI>>, người nhận: địa chỉ) { coin.do!(|c| transfer::public_transfer(c, người nhận)) /* mở rộng thành if (coin.is_some()) transfer::public_transfer(coin.destroy_some(), người nhận) else coin.destroy_none() */ }
hủy hoại
thực hiện cùng một hành động như do, nhưng có lẽ một macro hữu ích hơn là destroy_or cung cấp một biểu thức mặc định, chỉ được đánh giá nếu Option là none. Ví dụ sau đây liên kết map, áp dụng lambda cho giá trị của Option nếu nó là some, và destroy_or để chuyển đổi ngắn gọn Option<Coin<SUI>> thành Balance<SUI>.
vui to_balance_opt(coin: Option<Coin<SUI>>): Balance<SUI> { coin.map!(|c| c.into_balance()).destroy_or!(Balance::zero()) /* mở rộng thành let opt = if (coin.is_some()) Option::some(coins.destroy_some().into_balance()) else Option::none(); if (opt.is_some()) opt.destroy_some() else Balance::zero() */ }
Xem Bảng B ở cuối bài viết này để biết danh sách các hàm macro hiện có cho Option.
Kết thúc
Các hàm macro có thể đơn giản hóa mã của bạn bằng cách chuyển đổi các mẫu chung thành các macro có thể tái sử dụng. Hy vọng là các hàm macro có thể thay thế nhiều, nếu không muốn nói là tất cả, các vòng lặp chung của bạn (loop và while). Hãy chú ý đến nhiều hàm macro hơn trong thư viện move-stdlib và sớm có trong khuôn khổ sui.
Để biết thêm chi tiết về cách hoạt động của các hàm macro, hãy xem sách Move.
Bảng A: Các hàm macro hiện có cho vectơ
Thêm vào trước
công chúng vĩ mô vui vẻ
vào mỗi dòng bên dưới:
tabulate<$T>($n: u64, $f: |u64| -> $T): vector<$T>
Tạo một vector có độ dài
N
bằng cách gọi hàm f trên mỗi chỉ mục.
hủy<$T>($v: vector<$T>, $f: |$T|)
Phá hủy vector
v
bằng cách gọi f trên mỗi phần tử và sau đó phá hủy vectơ. Không bảo toàn thứ tự các phần tử trong vectơ (bắt đầu từ cuối vectơ).
do<$T>($v: vector<$T>, $f: |$T|)
Phá hủy vector
v
bằng cách gọi f trên mỗi phần tử và sau đó hủy vectơ. Bảo toàn thứ tự các phần tử trong vectơ.
do_ref<$T>($v: &vector<$T>, $f: |&$T|)
Thực hiện một hành động
f
trên mỗi phần tử của vectơ v. Vectơ không bị thay đổi.
do_mut<$T>($v: &mut vector<$T>, $f: |&mut $T|)
Thực hiện một hành động
f
trên mỗi phần tử của vectơ v. Hàm f/ sử dụng tham chiếu có thể thay đổi đến phần tử.
map<$T, $U>($v: vector<$T>, $f: |$T| -> $U): vector<$U>
Bản đồ vector
v
đến một vectơ mới bằng cách áp dụng hàm f cho mỗi phần tử. Bảo toàn thứ tự các phần tử trong vectơ, first được gọi là first.
map_ref<$T, $U>($v: &vector<$T>, $f: |&$T| -> $U): vector<$U>
Bản đồ vector
v
đến một vectơ mới bằng cách áp dụng hàm f cho mỗi phần tử. Bảo toàn thứ tự các phần tử trong vectơ, first được gọi là first.
bộ lọc<$T: thả>($v: vector<$T>, $f: |&$T| -> bool): vector<$T>
Lọc vector
v
bằng cách áp dụng hàm f cho mỗi phần tử. Trả về một vector mới chỉ chứa các phần tử mà f trả về giá trị true.
phân vùng<$T>($v: vector<$T>, $f: |&$T| -> bool): (vector<$T>, vector<$T>)
Chia vector
v
thành hai vectơ bằng cách áp dụng hàm f cho mỗi phần tử. Trả về một tuple chứa hai vectơ: vectơ đầu tiên chứa các phần tử mà f trả về true, và vectơ thứ hai chứa các phần tử mà f trả về false.
find_index<$T>($v: &vector<$T>, $f: |&$T| -> bool): Tùy chọn<u64>
Tìm chỉ số của phần tử đầu tiên trong vector
v
thỏa mãn điều kiện f. Trả về some(index) nếu tìm thấy phần tử như vậy, nếu không thì trả về none().
count<$T>($v: &vector<$T>, $f: |&$T| -> bool): u64
Đếm có bao nhiêu phần tử trong vector
v
thỏa mãn vị ngữ f.
gấp<$T, $Acc>($v: vector<$T>, $init: $Acc, $f: |$Acc, $T| -> $Acc): $Acc
Giảm vector
v
thành một giá trị duy nhất bằng cách áp dụng hàm f cho mỗi phần tử. Tương tự như fold_left trong Rust và reduce trong Python và JavaScript.
bất kỳ<$T>($v: &vector<$T>, $f: |&$T| -> bool): bool
Cho dù bất kỳ phần tử nào trong vector
v
thỏa mãn điều kiện f. Nếu vectơ trống, trả về false.
tất cả<$T>($v: &vector<$T>, $f: |&$T| -> bool): bool
Cho dù tất cả các phần tử trong vector
v
thỏa mãn điều kiện f. Nếu vectơ rỗng, trả về true.
zip_do<$T1, $T2>($v1: vector<$T1>, $v2: vector<$T2>, $f: |$T1, $T2|)
Phá hủy hai vector
v1
Và
v2
bằng cách gọi f đến từng cặp phần tử. Hủy bỏ nếu các vectơ không có cùng độ dài. Thứ tự các phần tử trong các vectơ được bảo toàn.
zip_do_reverse<$T1, $T2>($v1: vector<$T1>, $v2: vector<$T2>, $f: |$T1, $T2|)
Phá hủy hai vector
v1
Và
v2
bằng cách gọi f đến từng cặp phần tử. Hủy bỏ nếu các vectơ không có cùng độ dài. Bắt đầu từ cuối các vectơ.
zip_do_ref<$T1, $T2>($v1: &vector<$T1>, $v2: &vector<$T2>, $f: |&$T1, &$T2|)
Lặp lại qua
v1
Và
v2
và áp dụng hàm f cho các tham chiếu của mỗi cặp phần tử. Các vectơ không bị sửa đổi. Hủy bỏ nếu các vectơ không có cùng độ dài. Thứ tự các phần tử trong các vectơ được bảo toàn.
zip_do_mut<$T1, $T2>($v1: &mut vector<$T1>, $v2: &mut vector<$T2>, $f: |&mut $T1, &mut $T2|)
Lặp lại qua
v1
và v2 và áp dụng hàm f cho các tham chiếu có thể thay đổi của mỗi cặp phần tử. Các vectơ có thể được sửa đổi. Hủy bỏ nếu các vectơ không có cùng độ dài. Thứ tự các phần tử trong các vectơ được bảo toàn.
zip_map<$T1, $T2, $U>($v1: vector<$T1>, $v2: vector<$T2>, $f: |$T1, $T2| -> $U): vector<$U>
Phá hủy hai vector
v1
Và
v2
bằng cách áp dụng hàm f cho từng cặp phần tử. Các giá trị trả về được thu thập vào một vectơ mới. Hủy bỏ nếu các vectơ không có cùng độ dài. Thứ tự các phần tử trong các vectơ được bảo toàn.
zip_map_ref<$T1, $T2, $U>($v1: &vector<$T1>, $v2: &vector<$T2>, $f: |&$T1, &$T2| -> $U): vector<$U>
Lặp lại qua
v1
và v2 và áp dụng hàm f cho các tham chiếu của mỗi cặp phần tử. Các giá trị trả về được thu thập vào một vectơ mới. Hủy bỏ nếu các vectơ không có cùng độ dài. Thứ tự các phần tử trong các vectơ được bảo toàn.
Bảng B: Các hàm macro hiện có cho Option
Thêm vào trước
công chúng vĩ mô vui vẻ
vào mỗi dòng bên dưới:
hủy<$T>($o: Tùy chọn<$T>, $f: |$T|)
Hủy hoại
Tùy chọn<T>
và gọi hàm đóng f trên giá trị bên trong nếu nó giữ một giá trị.
do<$T>($o: Tùy chọn<$T>, $f: |$T|)
Hủy hoại
Tùy chọn<T>
và gọi hàm đóng f trên giá trị bên trong nếu nó giữ một giá trị.
do_ref<$T>($o: &Option<$T>, $f: |&$T|)
Thực hiện lệnh đóng trên giá trị bên trong
t
nếu nó chứa một.
do_mut<$T>($o: &mut Tùy chọn<$T>, $f: |&mut $T|)
Thực hiện lệnh đóng trên tham chiếu có thể thay đổi tới giá trị bên trong
t
nếu nó chứa một.
hoặc<$T>($o: Tùy chọn<$T>, $mặc định: Tùy chọn<$T>): Tùy chọn<$T>
Chọn đầu tiên
Một số
giá trị từ hai tùy chọn, hoặc None nếu cả hai đều là None. Tương đương với a.or(b) của Rust.
và<$T, $U>($o: Tùy chọn<$T>, $f: |$T| -> Tùy chọn<$U>): Tùy chọn<$U>
Nếu giá trị là
Một số
, gọi closure f trên đó. Nếu không, trả về None. Tương đương với t.and_then(f) của Rust.
và_ref<$T, $U>($o: &Option<$T>, $f: |&$T| -> Option<$U>): Option<$U>
Nếu giá trị là
Một số
, gọi hàm đóng f trên đó. Nếu không, trả về None. Tương đương với t.and_then(f) của Rust.
map<$T, $U>($o: Tùy chọn<$T>, $f: |$T| -> $U): Tùy chọn<$U>
Bản đồ một
Tùy chọn<T>
đến Option<U> bằng cách áp dụng một hàm cho một giá trị được chứa. Tương đương với t.map(f) của Rust.
map_ref<$T, $U>($o: &Option<$T>, $f: |&$T| -> $U): Option<$U>
Bản đồ một
Tùy chọn<T>
giá trị cho Option<U> bằng cách áp dụng một hàm cho một giá trị được chứa bằng tham chiếu. Option<T> gốc được bảo toàn. Tương đương với t.map(f) của Rust.
bộ lọc<$T: thả>($o: Tùy chọn<$T>, $f: |&$T| -> bool): Tùy chọn<$T>
Trở lại
Không có
nếu giá trị là None, nếu không thì trả về Option<T> nếu hàm f trả về true.
is_some_and<$T>($o: &Option<$T>, $f: |&$T| -> bool): bool
Trở lại
SAI
nếu giá trị là None, nếu không thì trả về kết quả của hàm f.
hủy_hoặc<$T>($o: Tùy chọn<$T>, $mặc định: $T): $T
Hủy hoại
Tùy chọn<T>
và trả về giá trị bên trong nếu nó chứa một giá trị, hoặc trả về giá trị mặc định nếu không. Tương đương với t.unwrap_or(default) của Rust.
Lưu ý: chức năng này là phiên bản hiệu quả hơn của
hủy_với_mặc_định
, vì nó không đánh giá giá trị mặc định trừ khi cần thiết. Hàm destroy_with_default nên bị loại bỏ để ủng hộ hàm này.