Julia 元編程

2018-08-12 21:25 更新

元編程

類似 Lisp ,Julia 自身的代碼也是語(yǔ)言本身的數(shù)據(jù)結(jié)構(gòu)。由于代碼是由這門語(yǔ)言本身所構(gòu)造和處理的對(duì)象所表示的,因此程序也可以轉(zhuǎn)換并生成自身語(yǔ)言的代碼。元編程的另一個(gè)功能是反射,它可以在程序運(yùn)行時(shí)動(dòng)態(tài)展現(xiàn)程序本身的特性。

表達(dá)式和求值

Julia 代碼表示為由 Julia 的 Expr 類型的數(shù)據(jù)結(jié)構(gòu)而構(gòu)成的語(yǔ)法樹。下面是 Expr 類型的定義:

type Expr
  head::Symbol
  args::Array{Any,1}
  typ
end

head 是標(biāo)明表達(dá)式種類的符號(hào);args 是子表達(dá)式數(shù)組,它可能是求值時(shí)引用變量值的符號(hào),也可能是嵌套的 Expr 對(duì)象,還可能是真實(shí)的對(duì)象值。 typ 域被類型推斷用來(lái)做類型注釋,通??梢员缓雎?。

有兩種“引用”代碼的方法,它們可以簡(jiǎn)單地構(gòu)造表達(dá)式對(duì)象,而不需要顯式構(gòu)造 Expr 對(duì)象。第一種是內(nèi)聯(lián)表達(dá)式,使用 : ,后面跟單表達(dá)式;第二種是代碼塊兒,放在 quote ... end 內(nèi)部。下例是第一種方法,引用一個(gè)算術(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

符號(hào)

: 的參數(shù)為符號(hào)時(shí),結(jié)果為 Symbol 對(duì)象,而不是 Expr

julia> :foo
:foo

julia> typeof(ans)
Symbol

在表達(dá)式的上下文中,符號(hào)用來(lái)指示對(duì)變量的讀取。當(dāng)表達(dá)式被求值時(shí),符號(hào)的值受限于符號(hào)的作用域(詳見(jiàn)變量的作用域)。

有時(shí), 為了防止解析時(shí)產(chǎn)生歧義,: 的參數(shù)需要添加額外的括號(hào):

julia> :(:)
:(:)

julia> :(::)
:(::)

Symbol 也可以使用 symbol 函數(shù)來(lái)創(chuàng)建,參數(shù)為一個(gè)字符或者字符串:

julia> symbol('\'')
:'

julia> symbol("'")
:'

求值和內(nèi)插

指定一個(gè)表達(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

每一個(gè)組件 有在它全局范圍內(nèi)評(píng)估計(jì)算表達(dá)式的 eval 表達(dá)式。傳遞給 eval 的表達(dá)式不限于返回一個(gè)值 - 他們也會(huì)具有改變封閉模塊的環(huán)境狀態(tài)的副作用:

julia> ex = :(x = 1)
:(x = 1)

julia> x
ERROR: x not defined

julia> eval(ex)
1

julia> x
1

表達(dá)式僅僅是一個(gè) Expr 對(duì)象,它可以通過(guò)編程構(gòu)造,然后對(duì)其求值:

julia> a = 1;

julia> ex = Expr(:call, :+,a,:b)
:(+(1,b))

julia> a = 0; b = 2;

julia> eval(ex)
3

注意上例中 ab 使用時(shí)的區(qū)別:

  • 表達(dá)式構(gòu)造時(shí),直接使用變量 a 的值。因此,對(duì)表達(dá)式求值時(shí) a 的值沒(méi)有任何影響:表達(dá)式中的值為 1,與現(xiàn)在 a 的值無(wú)關(guān)
  • 表達(dá)式構(gòu)造時(shí),使用的是符號(hào) :b 。因此,構(gòu)造時(shí)變量 b 的值是無(wú)關(guān)的—— :b 僅僅是個(gè)符號(hào),此時(shí)變量 b 還未定義。對(duì)表達(dá)式求值時(shí),通過(guò)查詢變量 b 的值來(lái)解析符號(hào) :b 的值

這樣構(gòu)造 Expr 對(duì)象太丑了。Julia 允許對(duì)表達(dá)式對(duì)象內(nèi)插。因此上例可寫為:

julia> a = 1;

julia> ex = :($a + b)
:(+(1,b))

編譯器自動(dòng)將這個(gè)語(yǔ)法翻譯成上面帶 Expr 的語(yǔ)法。

代碼生成

Julia 使用表達(dá)式內(nèi)插和求值來(lái)生成重復(fù)的代碼。下例定義了一組操作三個(gè)參數(shù)的運(yùn)算符: ::

for op = (:+, :*, :&, :|, :$)
  eval(quote
    ($op)(a,b,c) = ($op)(($op)(a,b),c)
  end)
end

上例可用 : 前綴引用格式寫的更精簡(jiǎn): ::

for op = (:+, :*, :&, :|, :$)
  eval(:(($op)(a,b,c) = ($op)(($op)(a,b),c)))
end

使用 eval(quote(...)) 模式進(jìn)行語(yǔ)言內(nèi)的代碼生成,這種方式太常見(jiàn)了。Julia 用宏來(lái)簡(jiǎn)寫這個(gè)模式: ::

for op = (:+, :*, :&, :|, :$)
  @eval ($op)(a,b,c) = ($op)(($op)(a,b),c)
end

@eval 宏重寫了這個(gè)調(diào)用,使得代碼更精簡(jiǎn)。 @eval 的參數(shù)也可以是塊代碼:

@eval begin
  # multiple lines
end

對(duì)非引用表達(dá)式進(jìn)行內(nèi)插,會(huì)引發(fā)編譯時(shí)錯(cuò)誤:

julia> $a + b
ERROR: unsupported or misplaced expression $

宏有點(diǎn)兒像編譯時(shí)的表達(dá)式生成函數(shù)。正如函數(shù)會(huì)通過(guò)一組參數(shù)得到一個(gè)返回值,宏可以進(jìn)行表達(dá)式的變換,這些宏允許程序員在最后的程序語(yǔ)法樹中對(duì)表達(dá)式進(jìn)行任意的轉(zhuǎn)化。調(diào)用宏的語(yǔ)法為:

@name expr1 expr2 ...
@name(expr1, expr2, ...)

注意,宏名前有 @ 符號(hào)。第一種形式,參數(shù)表達(dá)式之間沒(méi)有逗號(hào);第二種形式,宏名后沒(méi)有空格。這兩種形式不要記混。例如,下面的寫法的結(jié)果就與上例不同,它只向宏傳遞了一個(gè)參數(shù),此參數(shù)為多元組 (expr1, expr2, ...)

@name (expr1, expr2, ...)

程序運(yùn)行前, @name 展開函數(shù)會(huì)對(duì)表達(dá)式參數(shù)處理,用結(jié)果替代這個(gè)表達(dá)式。使用關(guān)鍵字 macro 來(lái)定義展開函數(shù):

macro name(expr1, expr2, ...)
    ...
    return resulting_expr
end

下例是 Julia 中 @assert 宏的簡(jiǎn)單定義:

macro assert(ex)
    return :($ex ? nothing : error("Assertion failed: ", $(string(ex))))
end

這個(gè)宏可如下使用:

julia> @assert 1==1.0

julia> @assert 1==0
ERROR: Assertion failed: 1 == 0
 in error at error.jl:22

宏調(diào)用在解析時(shí)被展開為返回的結(jié)果。這等價(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) 的時(shí)候,會(huì)被拼接為條件語(yǔ)句,而 string(:(1==1.0)) 會(huì)被替換成一個(gè)斷言。因此所有這些表達(dá)式構(gòu)成了程序的語(yǔ)法樹。然后在運(yùn)行期間,如果表達(dá)式為真,則返回 nothing,如果條件為假,一個(gè)提示語(yǔ)句將會(huì)表明這個(gè)表達(dá)式為假。注意,這里無(wú)法用函數(shù)來(lái)代替,因?yàn)樵诤瘮?shù)中只有值可以被傳遞,如果這么做的話我們無(wú)法在最后的錯(cuò)誤結(jié)果中得到具體的表達(dá)式是什么樣子的。

在標(biāo)準(zhǔn)庫(kù)中真實(shí)的 @assert 定義要復(fù)雜一些,它可以允許用戶去操作錯(cuò)誤信息,而不只是打印出來(lái)。和函數(shù)一樣宏也可以有可變參數(shù),我們可以看下面的這個(gè)定義:

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 分為兩種操作模式。如果只有一個(gè)參數(shù),表達(dá)式會(huì)被 msgs 捕獲為空,并且如上面所示作為一個(gè)更簡(jiǎn)單的定義。如果用戶填上第二個(gè)參數(shù), 這個(gè)參數(shù)會(huì)被作為打印參數(shù)而不是錯(cuò)誤的表達(dá)式。你可以在下面名為 macroexpand 的函數(shù)中檢查宏擴(kuò)展的結(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)

在實(shí)際的 @assert 宏定義中會(huì)有另一種情況:如果不僅僅是要打印 "a should equal b,",我們還想要打印它們的值呢?有些人可能天真的想插入字符串變量如:@assert a==b "a ($a) should equal b ($b)!",但是這個(gè)宏不會(huì)如我們所愿的執(zhí)行。你能看出是為什么嗎?回顧字符串的那一章,一個(gè)字符串的重寫函數(shù),請(qǐng)進(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)該得到一個(gè)面上的字符串 msg_body,這個(gè)宏接收整個(gè)表達(dá)式且需要如我們所期望的計(jì)算。這可以直接拼接成返回的表達(dá)式來(lái)作為 string 調(diào)用的一個(gè)參數(shù)。通過(guò)看 error.jl源碼得到完整的實(shí)現(xiàn)。

@assert 宏極大地通過(guò)宏替換實(shí)現(xiàn)了表達(dá)式的簡(jiǎn)化功能。

衛(wèi)生宏

衛(wèi)生宏是個(gè)更復(fù)雜的宏。一般來(lái)說(shuō),宏必須確保變量的引入不會(huì)和現(xiàn)有的上下文變量發(fā)送沖突。相反的,宏中的表達(dá)式作為參數(shù)應(yīng)該可以和上下文代碼有機(jī)的結(jié)合在一起,進(jìn)行交互。另一個(gè)令人關(guān)注的問(wèn)題是,當(dāng)宏用不同方式定義的時(shí)候是否被應(yīng)該稱為另一種模式。在這種情況下,我們需要確保所有的全局變量應(yīng)該被納入正確的模式中來(lái)。Julia 已經(jīng)在宏方面有了很大的優(yōu)勢(shì)相比其它語(yǔ)言(比如 C)。所有的變量(比如 @assert中的 msg)遵循這一標(biāo)準(zhǔn)。

來(lái)看一下 @time 宏,它的參數(shù)是一個(gè)表達(dá)式。它先記錄下時(shí)間,運(yùn)行表達(dá)式,再記錄下時(shí)間,打印出這兩次之間的時(shí)間差,它的最終值是表達(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)為私有臨時(shí)變量,而 time 是標(biāo)準(zhǔn)庫(kù)中的 time 函數(shù),而不是用戶可能使用的某個(gè)叫 time 的變量( println 函數(shù)也如此)。

Julia 宏展開機(jī)制是這樣解決命名沖突的。首先,宏結(jié)果的變量被分類為本地變量或全局變量。如果變量被賦值(且未被聲明為全局變量)、被聲明為本地變量、或被用作函數(shù)參數(shù)名,則它被認(rèn)為是本地變量;否則,它被認(rèn)為是全局變量。本地變量被重命名為一個(gè)獨(dú)一無(wú)二的名字(使用 gensym 函數(shù)產(chǎn)生新符號(hào)),全局變量被解析到宏定義環(huán)境中。

但還有個(gè)問(wèn)題沒(méi)解決??紤]下例:

module MyModule
import Base.@time

time() = ... # compute something

@time time()
end

此例中, ex 是對(duì) time 的調(diào)用,但它并不是宏使用的 time 函數(shù)。它實(shí)際指向的是 MyModule.time 。因此我們應(yīng)對(duì)要解析到宏調(diào)用環(huán)境中的 ex 代碼做修改。這是通過(guò) esc 函數(shù)的對(duì)表達(dá)式“轉(zhuǎn)義”完成的:

macro time(ex)
    ...
    local val = $(esc(ex))
    ...
end

這樣,封裝的表達(dá)式就不會(huì)被宏展開機(jī)制處理,能夠正確的在宏調(diào)用環(huán)境中解析。

必要時(shí)這個(gè)轉(zhuǎn)義機(jī)制可以用來(lái)“破壞”衛(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)準(zhǔn)字符串文本

字符串中曾討論過(guò)帶標(biāo)識(shí)符前綴的字符串文本被稱為非標(biāo)準(zhǔn)字符串文本,它們有特殊的語(yǔ)義。例如:

  • r"^\s*(?:#|$)" 生成正則表達(dá)式對(duì)象而不是字符串
  • b"DATA\xff\u2200" 是字節(jié)數(shù)組文本 [68,65,84,65,255,226,136,128]

事實(shí)上,這些行為不是 Julia 解釋器或編碼器內(nèi)置的,它們調(diào)用的是特殊名字的宏。例如,正則表達(dá)式宏的定義如下:

macro r_str(p)
  Regex(p)
end

因此,表達(dá)式 r"^\s*(?:#|$)" 等價(jià)于把下列對(duì)象直接放入語(yǔ)法樹:

Regex("^\\s*(?:#|\$)")

這么寫不僅字符串文本短,而且效率高:正則表達(dá)式需要被編譯,而 Regex 僅在 代碼編譯時(shí) 才構(gòu)造,因此僅編譯一次,而不是每次執(zhí)行都編譯。下例中循環(huán)中有一個(gè)正則表達(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)化的原因,上例依然不如使用宏高效。但有時(shí),不使用宏可能更方便:要對(duì)正則表達(dá)式內(nèi)插時(shí)必須使用這種麻煩點(diǎn)兒的方式;正則表達(dá)式模式本身是動(dòng)態(tài)的,每次循環(huán)迭代都會(huì)改變,生成新的正則表達(dá)式。

不止非標(biāo)準(zhǔn)字符串文本,命令文本語(yǔ)法( echo "Hello, $person")也是用宏實(shí)現(xiàn)的:

macro cmd(str)
  :(cmd_gen($shell_parse(str)))
end

當(dāng)然,大量復(fù)雜的工作被這個(gè)宏定義中的函數(shù)隱藏了,但是這些函數(shù)也是用 Julia 寫的。你可以閱讀源代碼,看看它如何工作。它所做的事兒就是構(gòu)造一個(gè)表達(dá)式對(duì)象,用于插入到你的程序的語(yǔ)法樹中。

反射

除了使用元編程語(yǔ)法層面的反思,朱麗亞還提供了一些其他的運(yùn)行時(shí)反射能力。

類型字段 數(shù)據(jù)類型的域的名稱(或模塊成員)可以使用 names 命令來(lái)詢問(wèn)。例如,給定以下類型:

type Point
  x::FloatingPoint
  y
end

names(Point) 將會(huì)返回指針 Any[:x, :y]。在一個(gè) Point 中每一個(gè)域的類型都會(huì)被存儲(chǔ)在指針對(duì)象的 types域中:

julia> typeof(Point)
DataType
julia> Point.types
(FloatingPoint,Any)

亞型

任何數(shù)據(jù)類型的直接亞型可以使用 subtypes(t::DataType) 來(lái)列表查看。例如,抽象數(shù)據(jù)類型 FloatingPoint 包含四種(具體的)亞型::

julia> subtypes(FloatingPoint)
4-element Array{Any,1}:
 BigFloat
 Float16
 Float32
 Float64

任何一個(gè)抽象的亞型也將被列入此列表中,但其進(jìn)一步的亞型則不會(huì);“亞型”的遞歸應(yīng)用程序允許建立完整的類型樹。

類型內(nèi)部

當(dāng)使用到 C 代碼接口時(shí)類型的內(nèi)部表示是非常重要的。isbits(T::DataType)T 存儲(chǔ)在 C 語(yǔ)言兼容定位時(shí)返回 true 。每一個(gè)域內(nèi)的補(bǔ)償量可以使用 fieldoffsets(T::DataType) 語(yǔ)句實(shí)現(xiàn)列表顯示。

函數(shù)方法

函數(shù)內(nèi)的所有方法可以通過(guò) methods(f::Function) 語(yǔ)句列表顯示出來(lái)。

函數(shù)表示

函數(shù)可以在幾個(gè)表示層次上實(shí)現(xiàn)內(nèi)部檢查。一個(gè)函數(shù)的更低形式在使用 code_lowered(f::Function, (Args...))時(shí)是可用的,而類型推斷的更低形式在使用 code_typed(f::Function, (Args...))時(shí)是可用的。

更接近機(jī)器的是,LLVM 的中間表示的函數(shù)是通過(guò) code_llvm(f::Function, (Args...)) 打印的,并且最終的由此產(chǎn)生的匯編指令在使用 code_native(f::Function, (Args...) 時(shí)是可用的。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)