iThome 網站首載:動態擴展語言元素的程式設計
無論是何種程式語言,都想要盡可能擁有廣大使用者,因而語法設計為適用多個開發領域,不針對特定用途的通用語言(General-Purpose Language),只是既然不針對特定用途設計,現有語言功能難免照顧不到特定需求,語言本身改進又曠日費時,因而有開發者以語言本身為處理對象進行meta-programming,擴展語言能力以符合特定需求。
- meta與meta-programming
meta前置詞源於希臘文,原本相當於之後(after)、超越(beyond)之意,後續衍生出許多意義,像是關於(about),例如metadata是有關資料的資料(data about data),像是資料庫表格用以儲存資料,而表格中各欄位是何種型態等描述,就稱為metadata,中文常見譯為「中繼資料」,或許因為如此,後來出現meta-programming一詞時,有些文件或書籍也就譯為中繼程式設計,然而這並不是一個適切的譯名。
meta前置詞其實也代表著更高階的抽象,在維基百科的Meta條目中舉的例子是,任何學科(Subject)可以說都有個更抽象層次的元理論(meta-theory),像是學科基礎、方法、形式等,meta-programming中的meta指的就是高階抽象,就如物件導向程式設計是以物件為處理對象,meta-programming是以語言中的抽象元素為處理對象,語言中的抽象元素有哪些呢?像是函式、類別、實例、方法等元素,甚至一段運算式、程式碼都可能是meta-programming的處理對象。
因而在維基百科對Metaprogramming的條目是,將其他程式或本身作為資料進行寫入或處理的電腦程式形式,因為是將其他程式或本身的元素作為資料,中文上比較適切且較廣為人能接受的譯名是「元程式設計」,較簡單易懂的解釋是「用程式產生程式」。就實作形式而言,meta-programming本身並沒有嚴謹的定義,將其他程式或本身作為資料進行寫入或處理的工作,可能是在編譯時期,也可能是在執行時期,因而像是C的巨集、執行時期修改函式、類別、實例、方法等元素定義或行為,甚至是用一個語言產生另一語言的程式產生器,都可視為meta-programming的一種形式。
在各種meta-programming形式中,執行時期修改函式、類別、實例、方法等元素定義與行為最具彈性,有些語言本身內建這樣的能力,開發者本身可以在原有語言上輕鬆構造、擴充專屬功能,像是Ruby、JavaScript、Groovy等,甚至可發展出一套具備特有語法的框架、程式庫或工具,就擴充的語法本身已成為特定領域語言(Domain specific language, DSL);有些語言則提供有限的動態能力,讓語言使用者維持一致的交流方式,避免產生溝通與維護上的問題。
- 豐富的meta-programming能力易於融入語言
既然meta-programming是以語言中的抽象元素為處理對象,那麼語法上可直接處理的元素越多,進行meta-programming就越方便,一級函式概念、基於原型(Prototype-based)或允許物件個體化(Object individuation)、提供反射(Reflection)或內省(Introspection)、可於執行時期直譯程式碼等機制,都是增加語言中處理對象的方式。動態語言通常具備有這樣的特性,其中最為人所知的就是Ruby語言,備齊了以上提及的機制,只要你願意,隨時可以基於現有程式基礎上產生新的程式功能,只是這有什麼用呢?舉例來說,
attr_reader
、attr_writer
或attr_accessor
等方法,就是在類別上動態產生實例變數的讀取與寫入方法。電腦科學家Peter Norvig曾於〈Design Patterns in Dynamic Programming〉的演講中指出,設計模式的目的之一,是用來避開實作語言本身的限制(To avoid limitations of implementation language)。以狹義的GoF設計模式來說,動態語言本身限制較少,不少模式在形式上用不到,而有些可以直接了當地用一級函式、MetaClass等來實作,如果語言本身在語法上支援,甚至可將模式實現透過巧妙設計,融入語言本身成為語法的一部份,最簡單的例子就是Ruby中的區塊(Block)機制,可實現像是以下的語法:
[1, 2, 3].each do |element|
print element
end
某些程度來說,程式庫或框架都可視為語言的延伸,如果設計模式可藉由支援meta-programming機制,融入而成為語言本身的一部份,那透過適當設計,程式庫或框架就可看起來像是處理特定問題或領域的小型語言。最有名的例子就是Rails,不少開發者都認為,與其將Rails說是一門框架,不如說它是門語言,一個用於處理Web應用程式開發領域的DSL,另一個例子就是Gradle,它的建構腳本(Build script)檔案,使用基於Groovy的動態功能而搭建出的DSL,實現了James Gosling曾說過的概念:「每個組態檔都會成為一門程式語言」。
- 有限的meta-programming能力減少意外性
語言的動態與靜態分野其實並沒有確切的定義,動靜程度只是相對而言,基本上,語法上可直接處理的元素越少,少到令人感到不便,就會被歸類到靜態語言,像是Java就是個例子,它基於類別(Class-based)、不允許物件個體化、沒有一級函式概念(JDK8之前),不過,Java中仍可以透過反射、泛型(Generic)與標註(Annotation)來進行有限的meta-programming,只是相對於動態語言來說,太費工也太不方便。
Java中不少動態行為,都得透過反射機制,像是AOP設計,或者透過標註來宣告、修正物件行為,不過這類動態功能在程式實作上都有一定難度,泛型則只提供極其有限的型態參數化能力,用以略為減輕型態負擔。有限的meta-programming能力,使得開發者沒有意外地必得用Java自身語法來撰寫程式,也使得設計模式在Java中顯得重要,因為模式名稱提供了統一的溝通名詞,就合作與管理的角度來說,倒也成了Java這類靜態語言的優點,因為語法上不容易出現意外性;不過就缺點來說,有限的meta-programming能力下,構築DSL也顯得困難,通常透過適當的模式與類別、方法命名等,來構築DSL的形式與語義,這使得Java在處理特定領域問題時,相對來說顯得麻煩而囉嗦。
許多時候動態定型語言會被簡稱為動態語言,不過要注意的是,這邊討論語言的靜態與動態並不是指定型,而是動態程度,也就是動態改變程式行為的能力,靜態定型語言不見得是靜態語言,例如Scala是靜態定型語言,不過相對於Java這類語言來說,還是具有很高的動態性,進行meta-programming相對來說容易許多,也較易構築融入語法的元素或整套DSL。
- meta-programming時留意共同慣例與詞彙
語言本身支援meta-programming的程度,其實就代表了語言自身的哲學與態度。Ruby的哲學中認為,解決問題的方式可以不只一種,也不認為本身能預測將來被應用的範圍,因而在meta-programming上提供豐富的支援,帶有解放Java限制使命的Groovy也是如此;然而,Python的哲學中認為,一件事最好只有一種解決方法,這樣可使用的工具集合就小,開發者可專注在解決問題上,因而Python某些程度上雖然可進行meta-programming,不過社群風氣上較不會去特意宣傳或使用這類特性。
從靜態語言限制meta-programming,搭配設計模式建立共通詞彙以減少意外性的角度來看,在動態語言中運用meta-programming等機制時,也應留意共同慣例與詞彙,可以輕易地動態改變程式行為,確實一件很誘人的事,但如果隨意改變行為、甚至任意構造融入語言的元素或特立獨行的DSL,會使得開發者之間的溝通變得困難或提高學習成本,就如同《蜘蛛人》電影中出現的台詞「能力越大、責任越重」,除非你精通該門語言,擁有足夠理由,能夠留意共同慣例與詞彙,否則最好別隨意搬弄meta-programming的相關機制。