從Java和JavaScript來學習Haskell和Groovy(元編程)
本篇文章的話題是元編程。首先來認識元編程,我在第一篇《引子》里面已經介紹:元編程,指的是在運行時改變“類”的定義,例如訪問、增加或修改等等。一言以蔽之,就是“用程序來寫程序”。在第二篇的《類型系統》里面已經借由繼承和接口的實現,介紹了一些利用元編程特性來增加或改變子類行為的方法。回顧語言發展的長河,其實是經歷了一個從“對象 -> 類 -> 元類”到“對象 -> 原型”的發展過程的。所以,無論是類,還是元類,這樣的概念其實都不是非有不可的,只是因為我們思考的習慣,特別是抽象的習慣而順其自然地產生了。這一點我在 《編程范型:工具的選擇》 里面已經詳細描述了,建議在往下閱讀前移步。
正式介入元編程的部分,先來看看Java,它的方式比較原始,也比較清晰,本身它定義了Class、Method、Field等等描述一個類的基本概念,基于靜態語言的限制,沒有辦法真正在運行時改變一個類內部的結構(但是可以在運行時獲取一個類內部的結構),于是有了像CGLib這樣在運行時使用動態代理,創建一個類來替代的辦法,讓使用者看起來好像是改變了原始類的結構。當然,在編譯期,像AspectJ這樣的工具可以做到真正的“織入”邏輯,控制字節碼的生成。對于Java的元編程本身而言,即便到今天,局限性很大,但是局限性并不意味著有用性,可以說如今元編程的應用已經鋪天蓋地,其中有這樣兩件事情大大加速了它元編程的發展:
- 一是JDK 5的注解,雖說它和元編程本身沒有直接的聯系,但是它提供了一種便捷的代碼修飾方式,也讓對于既有代碼的擴展變得方便而充滿可能。比如像Lombok這樣基于注解的類庫,讓一個類的擴展和完善非常容易。
- 二是Spring,無論是學J2ME還是J2EE,Spring都是值得去了解的,AOP的概念老早就提了,但就是從它開始發揚光大的;IoC,把對象管理和拼裝的邏輯反轉到業務邏輯之外的容器上,這些實現都是需要通過對元編程的操縱來完成的。
再來看看Haskell,把它和Java放在一起介紹,因為二者都是靜態語言,改變類或者定義結構的事情只能寄期望于編譯期完成。Haskell的元編程并非核心內容,因此也更加初級,據我所知,基本上談及Haskell的元編程,必談 Template Haskell (TH)。我對TH的了解屬于剛接觸,對于進一步了解,需要知曉這樣兩個概念,抽象語法樹(abstract syntax tree,AST),代碼語法分析成功以后就會生成AST,它包含的內容和代碼本身是一致的。而TH的執行結果,也是生成一棵AST。
接著要了解的概念是QuasiQuotation,里面可以存放任何字符串,被視作一個表達式,允許程序員寫自定義的結構片段(下面的中括號組合加上里面的豎線的這個結構 [| |])。比如(例子來自 這里 ):
silly :: QuasiQuoter silly = QuasiQuoter { quoteExp = \_ -> [| "yeah!!!" |] }
這一篇 介紹 相對比較容易理解(里面還介紹了使用reify來自省)。比如 [d|head’ (x:_) = x|] 這樣的表達式會被解析成為這樣一棵AST:[FunD head’ [Clause [InfixP (VarP x_1) GHC.Types.: WildP] (NormalB (VarE x_1)) []]]。
如果表達式里面還有Quotation,就需要使用 $() 來區分,比如:
emptyShow :: Name -> Q [Dec] emptyShow name = [d|instance Show $(conT name) where show _ = ""|]
無論是上面的哪一個,限制都還是太多了,主要的原因還是在于,它們是靜態語言;因此要用元編程用得自如,必須深入學習一門動態語言。
來看JavaScript。從靜態語言的囚籠中解脫出來, JavaScript的元編程的能力雖然強大,但是卻很容易歸納:
- 對對象的自省,對對象方法和屬性的改變,這里的對象既包括普通的對象和方法實例,也包括prototype這個特殊成員;
- eval關鍵字。
其余那些元編程的特性,都是其他人或者說第三方基于以上元編程基本的能力給后加上去的。
對于第一條,其實可以用下面這個最簡單的例子來概括:
function Func(){}; var func = new Func(); func.a = function(){ console.log("a"); }; Func.b = function(){ console.log("b"); }; Func.prototype.c = function(){ console.log("c"); }; // instance func.a(); // function Func.b(); // prototype func.c();
而對于第二條,還是用一個最簡單的例子來說明,數據和代碼等價的道理(還有一個關于模板引擎使用代碼生成的例子在這里):
var str = "{a:3, b:4}"; var obj = eval("(" + str + ")"); console.log(obj);
最后是Groovy,把Groovy放在最后是因為它的 元編程特性太豐富了 (下面的特性,如果要找例子都可以去這個官網的鏈接)。Java的所有元編程能力全部保留,在之基礎上,下面我有選擇地介紹幾條。
1、MethodMissing:這是一個我非常喜歡的特性,簡言之就是當被調用方法不存在時,可以執行的自定義方法,想一想,這相當于為對象提供了一個重要的特性:default行為。與methodMissing相對的,還有propertyMissing。
class Foo { def methodMissing(String name, def args) { return "this is me" } } assert new Foo().someUnknownMethod(42l) == 'this is me'
2、GroovyInterceptable:這個特性是給方法調用增加一層攔截邏輯,換句話說,是AOP的一種實現,比如:
class SimplePOGO implements GroovyInterceptable { void targetMethod(){ System.out.println("...") } def invokeMethod(String name, args){ System.out.println("${name} is being called") //Get the method that was originally called. def calledMethod = SimplePOGO.metaClass.getMetaMethod(name, args) calledMethod?.invoke(this, args) } } simplePogo = new SimplePOGO() simplePogo.targetMethod()
上面的例子調用targetMethod,但是攔截邏輯放在invokeMethod里面。
3、Categories,這是個從Objective C搬過來的特性。這個怎么說呢,很像電腦游戲里面角色的隱藏技能,平時不具備,但是危急關頭(使用use關鍵字)可以觸發打開,等到危急結束(use的代碼塊結束),技能又消失,恢復原狀。
use(TimeCategory) { println 1.minute.from.now println 10.hours.ago def someDate = new Date() println someDate - 3.months }
4、Magic Package。“魔法包”?聽起來就很牛的樣子,對吧。如果我們遵循magic package的命名規約,我們可以創建自定義的元類(MetaClass):
groovy.runtime.metaclass.[package].[class]MetaClass
比如我們要改變java.lang.String的邏輯,那就實現一個MetaClass,并且這個類的路徑是:
groovy.runtime.metaclass.java.lang.StringMetaClass
BTW,Groovy的 MetaClass的一系列子類 能力很強,連static method之類的東西都可以改變。更多的元編程特性,去官網找就好了。
但是回過頭來看一下,若論功能和特性的種類和紛繁程度,自然沒得說,但是從語言設計的簡潔性來說,JavaScript這個老被說“有缺陷”的語言卻可以甩Groovy幾條街。這并非一個孰好孰壞的評判,正如同接口的設計一樣,有人喜歡最簡接口,有人喜歡人本接口。有的語言就是喜歡簡潔,像我以前提過的Io語言,連關鍵字都省了;但是像Perl呢,卻說:
There’s More Than One Way To Do It.
所以,程序員啊,開心最重要了。(-_-)~…
在下一篇,會比較一下這四位DSL的特性和能力。