Trong khi lập trình, để rút ngắn và làm mã nguồn trong sạch, dễ đọc hơn, chúng ta thường viết các hàm với những chức răng riêng biệt. Tuy nhiên, đôi lúc viết hàm không phải là đủ. Khai báo hàm có thể cồng kềnh, bị lồng vào nhau, những cấu trúc lặp đi lặp lại nhưng không dùng hàm xử lý được… Những trường hợp đó là nơi macro tỏa sáng. May thay, những nhà khoa học MIT đã lường trước điều này và thiết kế Julia với một hệ thống macro - meta programming đầy đủ chức năng.
Sử dụng Macro
Macro được tận dụng khá nhiều trong Julia. Chúng thường được nhận biết bằng dấu @
ở đầu. Một số macro quen thuộc là:
@time
: đo độ trễ khi chạy một biểu thức@show
: hiện giá trị một biến@info
: hiện một thông báo với chữ màu@error
: quăng một lỗi, không dừng chương trình@warning
: quăng một cảnh báo, không dừng chương trình@.
: broadcast mọi hàm trong scope
Khi dùng macro, chúng ta đặt macro trước biểu thức cần tác động vào. Macro sẽ có hiệu lực trong biểu thức đứng trước nó. Ví dụ:
|
|
Ngoài ra, chúng ta còn có macro với đuôi _str
. Đây là những macro được thiết kế đặc biệt cho kiểu dữ liệu String, và có cách gọi rút gọn:
|
|
Để xem macro sẽ tạo ra đoạn code như nào, ta dùng macro @macroexpand
:
|
|
Từ ví dụ trên có thể thấy @. sum(...)
tác động tới cả hàm sum
vì sum
nằm trong biểu thức mà nó tác động vào, còn sum(@. ...)
thì không.
Tự tạo macro
Kiểu dữ liệu Expr
Khi một đoạn mã Julia được dịch, trước hết nó được đưa qua Abstract Syntax Tree (AST). Kiểu dữ liệu Expr
biểu diễn đoạn mã julia khi được đưa qua AST. Một Expr
gồm hai phần là head
và args
. Trong đó head
là phần cho biết đoạn mã nguồn thuộc loại biểu thức gì (gọi hàm, lặp, rẽ nhánh…); args
là một vector chứa những biểu thức con.
|
|
Hàm dump
in ra một cách trực quan cấu trúc của Expr
. Để tạo ra một Expr
, nếu biểu thức ngắn, chúng ta có thể bọc :(...)
quanh biểu thức.
ex = :(sum(c))
ex.head # :call
ex.args
# 2-element Array{Any,1}:
# :sum
# :c
Hoặc dùng khối quote
với một nhóm biểu thức hoặc một biểu thức dài:
ex = quote
a = 1
b = 2
a + b
end
ex.head # :block
ex.args
# 6-element Array{Any,1}:
# :(#= REPL[96]:2 =#)
# :(a = 1)
# :(#= REPL[96]:3 =#)
# :(b = 2)
# :(#= REPL[96]:4 =#)
# :(a + b)
Gọi Meta.parse
để chuyển một đoạn code về AST:
|
|
Cách cuối cùng là gọi trực tiếp Expr
với tham số là các biểu thức:
|
|
Mặc dù nhìn như lời gọi hàm như tất cả mọi thứ chỉ là biểu thức, do đó nếu có một biến số, hàm bên trong Expr
thì nó không cần được xác định cho tới khi Expr
được đánh giá. Để đánh giá một biểu thức ở dạng Expr
, chúng ta dùng hàm eval
.
|
|
Nếu muốn đưa một biến có sẵn vào trong Expr
ession, chúng ta dùng cú pháp nội suy giống trong xâu kí tự:
|
|
Có thể nói, bản thân mã nguồn cũng chỉ là một kiểu dữ liệu trong Julia. Do đó việc meta-programming trong Julia diễn ra rất tự nhiên.
Cách viết macro
Cú pháp của macro khá giống hàm
|
|
Cách hoạt động của macro có thể được hiểu như sau:
- Macro nhận biểu thức cần tác động qua tham số đầu tiên của nó dưới kiểu dữ liệu
Expr
- Phần thân macro biến đổi biểu thức, trả về một
Expr
khác - Julia lấy biểu thức
Expr
do macro trả về và đánh giá nó
Chúng ta có thể “thử” thông qua macro sau
|
|
Có thể thấy, macro @show
đã được gọi trong phần thân của m1
, kết quả trả về của @m1
là một biểu thức và được đánh giá ngay lập tức. Dưới đây là một macro mà tìm nghịch đảo của một biểu thức:
|
|
Thực ra, macro cũng có thể nhận nhiều tham số, tham số của macro được ngăn cách bằng dấu ,
, sẽ phải dùng ngoặc (trước ngoặc không có dấu cách), hoặc dùng dấu cách:
|
|
Dưới đây là một phiên bản khác của macro dùng quote
.
|
|
phần expression được bọc trong cú pháp nội suy $(...)
.
Macro hygiene
Macro trong những ví dụ trên chạy rất bình thường. Giả sử chúng ta đang viết một phần mềm, chúng ta muốn chia module, và có một module như sau
|
|
Sau khi chạy đoạn mã nguồn module trên, chúng ta nhập macro đó để sử dụng:
|
|
Nhưng đoạn mã nguồn sau sẽ gây lỗi:
|
|
Nhưng x
đã được định nghĩa, vậy chuyện gì đã xảy ra? Macro trên được gọi là “vệ sinh” (hygiene), tức là nó chỉ dùng những biến trong phạm vi mà macro đã được định nghĩa (trong module). Trong trường hợp này MacroExample.@inv
sẽ gọi tới MacroExample.x
(không tồn tại), dẫn đến lỗi như trên.
Giờ biến x
cần được trỏ tới x
trong môi trường gọi macro. Chúng ta có thể giải quyết vấn đề này bằng cách esc
(escape) nó. Macro @inv
sẽ được định nghĩa như sau:
|
|
Nói cách khác, macro này dùng biến trong môi trường gọi macro, tức là nó “không vệ sinh” (unhygiene). Một phiên bản gọn hơn của @inv
macro inv(ex)
Expr(:call, :(/), 1, esc(ex))
end
Vậy chuyện gì sẽ sảy ra nếu trong môi trường khai báo có một biến trùng tên với biến nội bộ của macro?
|
|
Câu trả lời là không có gì xảy ra, macro vẫn hoạt động bình thường, vì hàm gensym
của Julia tạo ra một biểu tượng độc nhất cho mỗi biến nội bộ của macro.
Macro & multiple dispatch
Vì macro giống function nên cũng có các method khác nhau, vẫn là ví dụ inv
:
|
|
Macro với cách gọi tắt cho string
Những macro có tên với đuôi _str
sẽ tự động có cách gọi tắt với String.
|
|
Đương nhiên ta vẫn có thể gọi theo cách thông thường:
|
|
Khi nào dùng macro
Trường hợp thường thấy khi dùng macro là macro cho string (@..._str
), trường hợp thường thấy tiếp theo là chúng ta có một cấu trúc code lặp đi lặp lại, nhưng lại là cấu trúc không thể dùng hàm để mô tả được. Khi đó macro sẽ giảm thiểu tối đa việc trùng lặp. Giả sử chúng ta đang cần một kiểu dữ liệu biểu diễn tổng của các số mà không làm mất đi thông tin các số hạng.
|
|
Giờ chúng ta muốn định nghĩa lại các hàm sơ cấp lượng giác trên kiểu dữ liệu này. Nếu không dùng macro chúng ta sẽ dùng như sau:
|
|
Nói chung là dài. Với macro, chúng ta có thể làm như sau (hãy mở lại Julia và định nghĩa lại struct trên trước khi thử)
|
|
Trong đó @evals x
là macro cho eval(:(x))
. Giờ thử với methodswith
:
|
|
Như vậy chúng ta với 4 dòng gọn gàng chúng ta đã định nghĩa được 16 method cho kiểu dữ liệu này mà không có dòng nào bị trùng lặp hay thừa.
Những macro hữu dụng
Đây không phải tất cả macro, trong các module khác có thể có nhiều macro hơn nữa. Macro khá ít so với hàm nên có thể được tìm nhanh bằng cách gõ @
và tab trên REPL.
Macro | chức năng |
---|---|
@time | tính thời gian thực thi |
@btime (từ Benchmark.jl) | tính thời gian thực thi, nhưng chính xác hơn và không bao gồm thời gian JIT |
@benchmark (từ Benchmark.jl) | đánh giá biểu thức, bao gồm thống kê thời gian thực thi và phân bố bộ nhớ |
@. | thực hiện broadcast (f.(x) ) trên tất cả các phép toán và hàm trong cùng phạm vi |
@simd | thực hiện simd cho vòng lặp, map và filter |
@inbounds | không kiểm tra chỉ số khi truy cập mảng, tăng tốc đáng kể trong trường hợp kiểm soát được chỉ số mảng |
@show,@info,@error | các macro hỗ trợ debug |
Base.@kwdef | định nghĩa struct với method khởi tạo dùng keyword |
@isdefined | kiểm tra xem một biến được định nghĩa hay chưa |
@deprecate | cảnh báo rằng một hàm, đoạn mã nguồn đã bị lỗi thời, không nên dùng |
@view/@views | tạo View khi lấy chỉ số của Array thay vì tạo ra một array mới |
@inline/@noinline | báo với compiler rằng một hàm có nên được inline hay không |
@DIR | đường dẫn thư mục chứa file mà macro này được gọi |
@FILE | đường dẫn file chứa lời gọi macro |
@MODULE | module mà code đang được gọi từ |
@which | xem một method, hàm, biến nằm trong module nào |
Kết
Macro có vẻ là một thức khá thần thánh và hữu dụng trong nhiều trường hợp. Tuy nhiên macro không nên bị làm dụng, đa số các tình huống có thể xử lý được (một cách đủ tốt) bằng hàm và có thể kết hợp với những macro có sẵn. Dùng macro bừa bãi có thể khiến chương trình thiếu ổn định (vì macro sinh mã nguồn nên khó kiểm soát), và một số trường hợp gây ảnh hưởng tới hiệu năng.