類似 Lisp ,Julia 自身的代碼也是語言本身的數(shù)據(jù)結(jié)構(gòu)。由于代碼是由這門語言本身所構(gòu)造和處理的對象所表示的,因此程序也可以轉(zhuǎn)換并生成自身語言的代碼。元編程的另一個功能是反射,它可以在程序運行時動態(tài)展現(xiàn)程序本身的特性。
Julia 代碼表示為由 Julia 的 Expr
類型的數(shù)據(jù)結(jié)構(gòu)而構(gòu)成的語法樹。下面是 Expr
類型的定義:
type Expr
head::Symbol
args::Array{Any,1}
typ
end
head
是標(biāo)明表達(dá)式種類的符號;args
是子表達(dá)式數(shù)組,它可能是求值時引用變量值的符號,也可能是嵌套的 Expr
對象,還可能是真實的對象值。 typ
域被類型推斷用來做類型注釋,通??梢员缓雎?。
有兩種“引用”代碼的方法,它們可以簡單地構(gòu)造表達(dá)式對象,而不需要顯式構(gòu)造 Expr
對象。第一種是內(nèi)聯(lián)表達(dá)式,使用 :
,后面跟單表達(dá)式;第二種是代碼塊兒,放在 quote ... end
內(nèi)部。下例是第一種方法,引用一個算術(shù)表達(dá)式:
julia> ex = :(a+b*c+1)
:(a + b * c + 1)
julia> typeof(ex)
Expr
julia> ex.head
:call
julia> typeof(ans)
Symbol
julia> ex.args
4-element Array{Any,1}:
:+
:a
:(b * c)
1
julia> typeof(ex.args[1])
Symbol
julia> typeof(ex.args[2])
Symbol
julia> typeof(ex.args[3])
Expr
julia> typeof(ex.args[4])
Int64
下例是第二種方法:
julia> quote
x = 1
y = 2
x + y
end
quote # none, line 2:
x = 1 # line 3:
y = 2 # line 4:
x + y
end
:
的參數(shù)為符號時,結(jié)果為 Symbol
對象,而不是 Expr
:
julia> :foo
:foo
julia> typeof(ans)
Symbol
在表達(dá)式的上下文中,符號用來指示對變量的讀取。當(dāng)表達(dá)式被求值時,符號的值受限于符號的作用域(詳見變量的作用域)。
有時, 為了防止解析時產(chǎn)生歧義,:
的參數(shù)需要添加額外的括號:
julia> :(:)
:(:)
julia> :(::)
:(::)
Symbol
也可以使用 symbol
函數(shù)來創(chuàng)建,參數(shù)為一個字符或者字符串:
julia> symbol('\'')
:'
julia> symbol("'")
:'
指定一個表達(dá)式,Julia 可以使用 eval
函數(shù)在 global 作用域?qū)ζ淝笾怠?/p>
julia> :(1 + 2)
:(1 + 2)
julia> eval(ans)
3
julia> ex = :(a + b)
:(a + b)
julia> eval(ex)
ERROR: a not defined
julia> a = 1; b = 2;
julia> eval(ex)
3
每一個組件 有在它全局范圍內(nèi)評估計算表達(dá)式的 eval
表達(dá)式。傳遞給 eval
的表達(dá)式不限于返回一個值 - 他們也會具有改變封閉模塊的環(huán)境狀態(tài)的副作用:
julia> ex = :(x = 1)
:(x = 1)
julia> x
ERROR: x not defined
julia> eval(ex)
1
julia> x
1
表達(dá)式僅僅是一個 Expr
對象,它可以通過編程構(gòu)造,然后對其求值:
julia> a = 1;
julia> ex = Expr(:call, :+,a,:b)
:(+(1,b))
julia> a = 0; b = 2;
julia> eval(ex)
3
注意上例中 a
與 b
使用時的區(qū)別:
a
的值。因此,對表達(dá)式求值時 a
的值沒有任何影響:表達(dá)式中的值為 1
,與現(xiàn)在 a
的值無關(guān):b
。因此,構(gòu)造時變量 b
的值是無關(guān)的—— :b
僅僅是個符號,此時變量 b
還未定義。對表達(dá)式求值時,通過查詢變量 b
的值來解析符號 :b
的值這樣構(gòu)造 Expr
對象太丑了。Julia 允許對表達(dá)式對象內(nèi)插。因此上例可寫為:
julia> a = 1;
julia> ex = :($a + b)
:(+(1,b))
編譯器自動將這個語法翻譯成上面帶 Expr
的語法。
Julia 使用表達(dá)式內(nèi)插和求值來生成重復(fù)的代碼。下例定義了一組操作三個參數(shù)的運算符: ::
for op = (:+, :*, :&, :|, :$)
eval(quote
($op)(a,b,c) = ($op)(($op)(a,b),c)
end)
end
上例可用 :
前綴引用格式寫的更精簡: ::
for op = (:+, :*, :&, :|, :$)
eval(:(($op)(a,b,c) = ($op)(($op)(a,b),c)))
end
使用 eval(quote(...))
模式進(jìn)行語言內(nèi)的代碼生成,這種方式太常見了。Julia 用宏來簡寫這個模式: ::
for op = (:+, :*, :&, :|, :$)
@eval ($op)(a,b,c) = ($op)(($op)(a,b),c)
end
@eval
宏重寫了這個調(diào)用,使得代碼更精簡。 @eval
的參數(shù)也可以是塊代碼:
@eval begin
# multiple lines
end
對非引用表達(dá)式進(jìn)行內(nèi)插,會引發(fā)編譯時錯誤:
julia> $a + b
ERROR: unsupported or misplaced expression $
宏有點兒像編譯時的表達(dá)式生成函數(shù)。正如函數(shù)會通過一組參數(shù)得到一個返回值,宏可以進(jìn)行表達(dá)式的變換,這些宏允許程序員在最后的程序語法樹中對表達(dá)式進(jìn)行任意的轉(zhuǎn)化。調(diào)用宏的語法為:
@name expr1 expr2 ...
@name(expr1, expr2, ...)
注意,宏名前有 @
符號。第一種形式,參數(shù)表達(dá)式之間沒有逗號;第二種形式,宏名后沒有空格。這兩種形式不要記混。例如,下面的寫法的結(jié)果就與上例不同,它只向宏傳遞了一個參數(shù),此參數(shù)為多元組 (expr1, expr2, ...)
:
@name (expr1, expr2, ...)
程序運行前, @name
展開函數(shù)會對表達(dá)式參數(shù)處理,用結(jié)果替代這個表達(dá)式。使用關(guān)鍵字 macro
來定義展開函數(shù):
macro name(expr1, expr2, ...)
...
return resulting_expr
end
下例是 Julia 中 @assert
宏的簡單定義:
macro assert(ex)
return :($ex ? nothing : error("Assertion failed: ", $(string(ex))))
end
這個宏可如下使用:
julia> @assert 1==1.0
julia> @assert 1==0
ERROR: Assertion failed: 1 == 0
in error at error.jl:22
宏調(diào)用在解析時被展開為返回的結(jié)果。這等價于:
1==1.0 ? nothing : error("Assertion failed: ", "1==1.0")
1==0 ? nothing : error("Assertion failed: ", "1==0")
上面的代碼的意思是,當(dāng)?shù)谝淮握{(diào)用表達(dá)式 :(1==1.0)
的時候,會被拼接為條件語句,而 string(:(1==1.0))
會被替換成一個斷言。因此所有這些表達(dá)式構(gòu)成了程序的語法樹。然后在運行期間,如果表達(dá)式為真,則返回 nothing
,如果條件為假,一個提示語句將會表明這個表達(dá)式為假。注意,這里無法用函數(shù)來代替,因為在函數(shù)中只有值可以被傳遞,如果這么做的話我們無法在最后的錯誤結(jié)果中得到具體的表達(dá)式是什么樣子的。
在標(biāo)準(zhǔn)庫中真實的 @assert
定義要復(fù)雜一些,它可以允許用戶去操作錯誤信息,而不只是打印出來。和函數(shù)一樣宏也可以有可變參數(shù),我們可以看下面的這個定義:
macro assert(ex, msgs...)
msg_body = isempty(msgs) ? ex : msgs[1]
msg = string("assertion failed: ", msg_body)
return :($ex ? nothing : error($msg))
end
現(xiàn)在根據(jù)參數(shù)的接收數(shù)目我們可以把 @assert
分為兩種操作模式。如果只有一個參數(shù),表達(dá)式會被 msgs
捕獲為空,并且如上面所示作為一個更簡單的定義。如果用戶填上第二個參數(shù), 這個參數(shù)會被作為打印參數(shù)而不是錯誤的表達(dá)式。你可以在下面名為 macroexpand
的函數(shù)中檢查宏擴展的結(jié)果:
julia> macroexpand(:(@assert a==b))
:(if a == b
nothing
else
Base.error("assertion failed: a == b")
end)
julia> macroexpand(:(@assert a==b "a should equal b!"))
:(if a == b
nothing
else
Base.error("assertion failed: a should equal b!")
end)
在實際的 @assert
宏定義中會有另一種情況:如果不僅僅是要打印 "a should equal b,",我們還想要打印它們的值呢?有些人可能天真的想插入字符串變量如:@assert a==b "a ($a) should equal b ($b)!"
,但是這個宏不會如我們所愿的執(zhí)行。你能看出是為什么嗎?回顧字符串的那一章,一個字符串的重寫函數(shù),請進(jìn)行比較:
julia> typeof(:("a should equal b"))
ASCIIString (constructor with 2 methods)
julia> typeof(:("a ($a) should equal b ($b)!"))
Expr
julia> dump(:("a ($a) should equal b ($b)!"))
Expr
head: Symbol string
args: Array(Any,(5,))
1: ASCIIString "a ("
2: Symbol a
3: ASCIIString ") should equal b ("
4: Symbol b
5: ASCIIString ")!"
typ: Any
所以現(xiàn)在不應(yīng)該得到一個面上的字符串 msg_body
,這個宏接收整個表達(dá)式且需要如我們所期望的計算。這可以直接拼接成返回的表達(dá)式來作為 string
調(diào)用的一個參數(shù)。通過看 error.jl源碼得到完整的實現(xiàn)。
@assert
宏極大地通過宏替換實現(xiàn)了表達(dá)式的簡化功能。
衛(wèi)生宏是個更復(fù)雜的宏。一般來說,宏必須確保變量的引入不會和現(xiàn)有的上下文變量發(fā)送沖突。相反的,宏中的表達(dá)式作為參數(shù)應(yīng)該可以和上下文代碼有機的結(jié)合在一起,進(jìn)行交互。另一個令人關(guān)注的問題是,當(dāng)宏用不同方式定義的時候是否被應(yīng)該稱為另一種模式。在這種情況下,我們需要確保所有的全局變量應(yīng)該被納入正確的模式中來。Julia 已經(jīng)在宏方面有了很大的優(yōu)勢相比其它語言(比如 C)。所有的變量(比如 @assert
中的 msg
)遵循這一標(biāo)準(zhǔn)。
來看一下 @time
宏,它的參數(shù)是一個表達(dá)式。它先記錄下時間,運行表達(dá)式,再記錄下時間,打印出這兩次之間的時間差,它的最終值是表達(dá)式的值:
macro time(ex)
return quote
local t0 = time()
local val = $ex
local t1 = time()
println("elapsed time: ", t1-t0, " seconds")
val
end
end
t0
, t1
, 及 val
應(yīng)為私有臨時變量,而 time
是標(biāo)準(zhǔn)庫中的 time
函數(shù),而不是用戶可能使用的某個叫 time
的變量( println
函數(shù)也如此)。
Julia 宏展開機制是這樣解決命名沖突的。首先,宏結(jié)果的變量被分類為本地變量或全局變量。如果變量被賦值(且未被聲明為全局變量)、被聲明為本地變量、或被用作函數(shù)參數(shù)名,則它被認(rèn)為是本地變量;否則,它被認(rèn)為是全局變量。本地變量被重命名為一個獨一無二的名字(使用 gensym
函數(shù)產(chǎn)生新符號),全局變量被解析到宏定義環(huán)境中。
但還有個問題沒解決??紤]下例:
module MyModule
import Base.@time
time() = ... # compute something
@time time()
end
此例中, ex
是對 time
的調(diào)用,但它并不是宏使用的 time
函數(shù)。它實際指向的是 MyModule.time
。因此我們應(yīng)對要解析到宏調(diào)用環(huán)境中的 ex
代碼做修改。這是通過 esc
函數(shù)的對表達(dá)式“轉(zhuǎn)義”完成的:
macro time(ex)
...
local val = $(esc(ex))
...
end
這樣,封裝的表達(dá)式就不會被宏展開機制處理,能夠正確的在宏調(diào)用環(huán)境中解析。
必要時這個轉(zhuǎn)義機制可以用來“破壞”衛(wèi)生,從而引入或操作自定義變量。下例在調(diào)用環(huán)境中宏將 x
設(shè)置為 0 :
macro zerox()
return esc(:(x = 0))
end
function foo()
x = 1
@zerox
x # is zero
end
應(yīng)審慎使用這種操作。
字符串中曾討論過帶標(biāo)識符前綴的字符串文本被稱為非標(biāo)準(zhǔn)字符串文本,它們有特殊的語義。例如:
r"^\s*(?:#|$)"
生成正則表達(dá)式對象而不是字符串b"DATA\xff\u2200"
是字節(jié)數(shù)組文本 [68,65,84,65,255,226,136,128]
事實上,這些行為不是 Julia 解釋器或編碼器內(nèi)置的,它們調(diào)用的是特殊名字的宏。例如,正則表達(dá)式宏的定義如下:
macro r_str(p)
Regex(p)
end
因此,表達(dá)式 r"^\s*(?:#|$)"
等價于把下列對象直接放入語法樹:
Regex("^\\s*(?:#|\$)")
這么寫不僅字符串文本短,而且效率高:正則表達(dá)式需要被編譯,而 Regex
僅在 代碼編譯時 才構(gòu)造,因此僅編譯一次,而不是每次執(zhí)行都編譯。下例中循環(huán)中有一個正則表達(dá)式:
for line = lines
m = match(r"^\s*(?:#|$)", line)
if m == nothing
# non-comment
else
# comment
end
end
如果不想使用宏,要使上例只編譯一次,需要如下改寫:
re = Regex("^\\s*(?:#|\$)")
for line = lines
m = match(re, line)
if m == nothing
# non-comment
else
# comment
end
end
由于編譯器優(yōu)化的原因,上例依然不如使用宏高效。但有時,不使用宏可能更方便:要對正則表達(dá)式內(nèi)插時必須使用這種麻煩點兒的方式;正則表達(dá)式模式本身是動態(tài)的,每次循環(huán)迭代都會改變,生成新的正則表達(dá)式。
不止非標(biāo)準(zhǔn)字符串文本,命令文本語法( echo "Hello, $person"
)也是用宏實現(xiàn)的:
macro cmd(str)
:(cmd_gen($shell_parse(str)))
end
當(dāng)然,大量復(fù)雜的工作被這個宏定義中的函數(shù)隱藏了,但是這些函數(shù)也是用 Julia 寫的。你可以閱讀源代碼,看看它如何工作。它所做的事兒就是構(gòu)造一個表達(dá)式對象,用于插入到你的程序的語法樹中。
除了使用元編程語法層面的反思,朱麗亞還提供了一些其他的運行時反射能力。
類型字段 數(shù)據(jù)類型的域的名稱(或模塊成員)可以使用 names
命令來詢問。例如,給定以下類型:
type Point
x::FloatingPoint
y
end
names(Point)
將會返回指針 Any[:x, :y]
。在一個 Point
中每一個域的類型都會被存儲在指針對象的 types
域中:
julia> typeof(Point)
DataType
julia> Point.types
(FloatingPoint,Any)
亞型
任何數(shù)據(jù)類型的直接亞型可以使用 subtypes(t::DataType)
來列表查看。例如,抽象數(shù)據(jù)類型 FloatingPoint
包含四種(具體的)亞型::
julia> subtypes(FloatingPoint)
4-element Array{Any,1}:
BigFloat
Float16
Float32
Float64
任何一個抽象的亞型也將被列入此列表中,但其進(jìn)一步的亞型則不會;“亞型”的遞歸應(yīng)用程序允許建立完整的類型樹。
類型內(nèi)部
當(dāng)使用到 C 代碼接口時類型的內(nèi)部表示是非常重要的。isbits(T::DataType)
在 T
存儲在 C 語言兼容定位時返回 true 。每一個域內(nèi)的補償量可以使用 fieldoffsets(T::DataType)
語句實現(xiàn)列表顯示。
函數(shù)方法
函數(shù)內(nèi)的所有方法可以通過 methods(f::Function)
語句列表顯示出來。
函數(shù)表示
函數(shù)可以在幾個表示層次上實現(xiàn)內(nèi)部檢查。一個函數(shù)的更低形式在使用 code_lowered(f::Function, (Args...))
時是可用的,而類型推斷的更低形式在使用 code_typed(f::Function, (Args...))
時是可用的。
更接近機器的是,LLVM 的中間表示的函數(shù)是通過 code_llvm(f::Function, (Args...))
打印的,并且最終的由此產(chǎn)生的匯編指令在使用 code_native(f::Function, (Args...)
時是可用的。
更多建議: