在探討現代程式語言到達一定的深度之後,往往會開始討論值、變數、範圍鏈、繼承鏈,甚至於反射(Reflection)、自省(Introspection)等meta-programming的原理。

若曾經試著實作一門語言,再從實作角度看這些特性,就不再覺得霧裡看花,而是有趣而實用了。

值與變數

在程式語言中,值是什麼呢?就語言本身來說,值會是數字、布林、字串,或者是物件這類東西;在具備一級函式的語言中,函式也會是個值;進一步延伸的話,類別也會是個值,而方法也會是個值,例如JavaScript或Python——物件上的方法,實際上是個函式,說它是值並不為過。

然而,從語言實作上來看,任何抽象語法樹上的節點,本質上都可以當成是值,一個語法節點,除了封裝了底層操作細節,還會維繫著與其他節點的關係,例如,一個函式節點,包含了函式本體的陳述句,而這些陳述句也會是語法節點;如果語言在特性願意開放,函式本體的陳述句也可以是個值。

只不過語言在一開始就暴露太多語法節點的話,勢必影響語言的易用性,因此,大多數語言,會謹慎選擇常用的語法節點,供開發者使用,像是數字、布林、字串、物件等,高階一些的語言,就開放函式,而取得函式本體的陳述句這類的操作,多數場合中並沒有作用,就被語言隱藏起來了。

既然抽象語法樹上的節點,都可以當成是值,那麼,變數也不例外。在語言實作中,往往會為變數建立語法節點,當程式運行時,變數節點主要的目的,是查找環境物件中是否有指定之名稱,若有的話、是否具備對應的節點物件,這些節點物件通常會是數字、布林、字串等節點物件。不過,若是要實現惰性求值,變數節點也可以直接對應另一變數節點,而不是立即求得後者的值。

覺得奇怪嗎?變數怎麼會是值?這是從抽象語法樹的觀點來看。實際上,有的語言確實能讓開發者查找變數節點,並以適當方式呈現。例如Python的dir函式,若不指定對象,會查找目前環境物件中的變數,並以字串清單轉回,JavaScript的全域物件,本質上,也就是環境物件,Ruby的methods方法會傳回物件可用的方法符號(Symbol),某些程度上,這些都是語言暴露給開發者,令其有機會接觸語法節點的一種機制。

陳述句與函式

陳述句被當成值又是怎麼回事?我們先來談談指定陳述。例如,x = 1這樣的指定陳述,實作上,可以建立new VariableAssign(new Variable('x'), new Num(1))語法節點(Variable就是方才談到的變數節點),而這節點就是陳述句節點,在執行時,會在當時環境物件,增加Variable('x')與Num(1)的對應。

來想想看函式吧!函式是一組陳述句的集合,然而,參數實際上還沒有對應的值,呼叫函式時,要怎麼套用引數呢?若函式上有個x參數,呼叫函式時指定為1,在實作上,會建立一個VariableAssign節點,並取得函式本體的陳述句,兩者結合成一個新的陳述句節點,例如:new StmtSequence(variableAssign, bodyStmt)。

如果執行這個節點,就會先執行variableAssign節點,然後才是函式本體的bodyStmt陳述句節點。由於函式上可能會有多個參數,呼叫函式時指定的引數也就會有多個,因此,參數或引數都會設計為清單的形式方便對應。如果語言願意開放,開發者可以取得參數清單,甚至於引數清單,例如,Python若在函式中使用dir函式,就可以取得參數清單,而JavaScript函式呼叫時,函式內部會有個arguments,參考至全部引數的清單。

函式呼叫時,實作上是主動取得陳述句,這是從語言實作角度來看,有沒有語言開放可以傳入陳述句?有的!Scala的by-value paramenter特性,例如:

def foo(stmt: => Any) = {
println("foo")
stmt
}
foo(println("XD"))

不同於其他語言特性,上面的程式顯示順序並不是XD、foo,而是foo與XD。

因為println("XD")被當成值傳入,而執行是放在println("foo")之後。當然,使用回呼,也可以達到相同目的(Scala本身也是以語法蜜糖方式實作),然而,若語言上開放,在需要傳送陳述句時,運用by-value parameter之類的特性會方便許多。

物件與實例

初學物件導向語言時,是否對於物件(Object)與實例(Instance)的差別而困惑,甚至跟人爭論?

就語言層面,可以產生一個物件,然而,它不是任何語言規範中型態之實例。因為在底層實作中,物件可以單純只是封裝了成對的鍵值,不需與語言中的任何型態產生關係,而嚴謹的語言通常會避免這種情況,所以,多數的場合中,這兩個名詞指的是同一個東西。

多數物件導向語言都會談到,若定義了一個類別,類別定義的方法並不會被個別實例擁有,透過實例呼叫方法時,會查找類別上是否有定義該方法而後呼叫,這就是在說明該語言對方法查找的實作方式。然而,如果語言支援物件單體化,物件本身也可以擁有方法,查找方法時,就會先用物件上之方法,若無再使用類別定義之方法。

JavaScript就是這樣的機制,雖然一開始沒有類別,然而,當函式作為建構式使用時,查找方法的順序,就是物件、建構式原型,接著就是建構式原型之原型,這就是(原型)繼承鏈了。有些語言可以明確存取、修改繼承鏈,JavaScript是其中之一,Python中也有__bases__、__mro__可以使用。

方法中的this是怎麼回事呢?就是個變數而已,只不過綁定了物件本身,在Python中,方法首個參數必須接受物件本身,單純就只是將語言實作時的綁定動作明確化罷了,JavaScript的函式則有call、apply可以使用,能在呼叫時,明確指定this綁定的物件,如果語言本身沒有開放這種機制,那麼就是隱含地進行綁定的動作。

有些較嚴謹的語言,直接開放的底層機制不多,然而,還是可以透過迂迴的方式來存取底層的語言實作。例如,Java可透過反射,取得Method、指定this等,以便完成許多語言本身語法做不到的事情。

meta-programming的本質

有些語言易於進行meta-programming,若仔細看看,提供的機制當中,有些就是上頭談過的東西,也就是說,一個語言暴露的底層機制越多,就越易於進行meta-programming,調整語言來施展魔法。

從這點來看,私下猜測當年Brendan Eich以十天創造JavaScript,因為時間倉促,許多沒考慮到的臭蟲,被開發者適應而成為(奇怪的)特性留存下來,而JavaScript易於修補的原因,是因為在語言半成品時就上戰場了,許多底層實作的細節沒有被隱藏起來,而被開發者拿來運用了。

如果試著實作語言,可能就會發現meta-programming,本質上就是利用語言特意(或不小心)暴露的底層實作細節,讓開發者進一步對語言進行深層次的操作,從而對語言增添特性的機制。

而從語言實作角度來看待語言中的相關特性,就是有這麼多的體會與樂趣,若要從事meta-programming動作,也就更能掌握必要的細節。

專欄作者

熱門新聞

Advertisement