Metaprogramming
is the writing of computer programs
that write or manipulate other programs (or themselves) as their data,
or that do part of the work at compile time
that would otherwise be done at runtime
. In some cases, this allows programmers to minimize the number of lines of code to express a solution (hence reducing development time), or it gives programs greater flexibility to efficiently handle new situations without recompilation.
沒有接觸過meta programming, 感覺"manipulate other programs (or themselves) as their data"這個說法有點虛, 能否給一些確切的例子說明其妙用之處?
另外, 為什麼說LISP與Ruby在meta programming上比Python做得要好?
回複內容:
如果把元編程定義為對程式進行操作的編程,那麼,似乎沒有理由把eval排除於元編程之外。
至於到底是應該編譯期還是運行期,我覺得還真是無所謂的。
元編程顯然是多提供了一個抽象層次,相當於讓你不僅可以通過一個層次去抽象問題,還能通過另一個層次對你的問題模型進行抽象。如果用的好,這似乎提供了巨大的能量。不過,這也似乎超出了大多數人的腦部運作規則,不太適合大面積推廣。
至於monad,如果專註於它可以重定義;也就是定義運算序列的能力的話,也勉強可以算是元編程吧。
說個題外話:
上面有人提到REBOL,說它的元編程不好,我的感覺明顯不同。由於REBOL採用最簡文法作為它的form,這就導致了自訂dialect可以做到非常自然和直觀,因而其元編程更自然,更漂亮。Ruby元編程這本書的引言就講的其實挺清楚的。
我到是建議你看完這本書,相信你能夠理解的,裡面例子還不錯,這本書我還沒看完。我現在手邊就是。
順便說說Lisp,大家都知道“代碼及資料”這回事,其實就是這樣子一回事。元編程的定義就是寫出編寫代碼的代碼,lisp裡面quote可以阻止求值,到需要使用的時候用eval就行了。然後在啟動並執行時候操作自己的代碼也是他另一個含義。可以call 它叫動態元編程,以此區分代碼產生器以及編譯器方式的靜態元編程。批量產生各種代碼(類,方法,測試等等),減少編程時的重複操作。各種DSL,code generator,scalfolding,project bootstraping都受益於meta programming。編程(programming)是對於某類問題解決方案的抽象。
而元編程(meta programming)則是對於某類解決方案的抽象。
也就是說,元編程是對於編程的抽象。
至於說到DSL這部分我不敢苟同,誠然,ruby圈的DSL有泛濫的趨勢,為了DSL而創造DSL、為了炫技而DSL,但是這些不是重點。DSL的出現是高度抽象的必然結果,脫離了抽象的DSL自然會死亡。
舉幾個栗子:
ORM
我有現在有N個實體類,我需要把這些實體類持久化儲存。所以我寫了N個DAO。後來發現,DAO這些東西好像很無聊嘛,無非就是那麼幾步,根據實體類名稱建立表,根據屬性建立資料庫欄位,然後增刪改查。(ORM)
為瞭解決這些“相似的重複勞動”,我們需要某種機制,能夠將我的一個"meta program"轉化成具體的某個program。這就是抽象層次的差別。
ORM的基本步驟:根據實體類類名確定資料庫表名,根據實體類屬性確定資料庫欄位,統一的insert\delete\find\update 方法,根據屬性與資料庫欄位的對應關係完成資料恢複與儲存。在java中,我們通過反射拿到類名和資料庫欄位,利用泛型+整合得到insert\delete\find\update以及各種公用方法。結果就是你只需要簡單繼承一個父類,就可以完成DAO的操作。 (GitHub - satyan/sugar: Insanely easy way to work with Android Database.
這裡有一個例子)
文法分析與詞法分析
文法分析與詞法也是一類具有很大相似性但是又有區別的事情。一個典型的例子就是lex&yacc(flex&bison), lex是一個詞法分析器,yacc是一個文法的分析器(“分析器”的措辭可能有些不嚴謹)。對於詞法/文法分析而言所以,lex接受一個.l檔案,yacc接受一個.y檔案,他們的結果是產生.c代碼。通過DSL來產生代碼,這是編譯型語言做的事情(對於c++瞭解不多,尤其是c++新標準引入了許多新特性,
據說可以達到動態語言一樣的便捷程度)分析器的本質是一個有限狀態機器,狀態機器的核心工作應該是劃分狀態,通過meta programming,我們可以避免編寫狀態機器的“重複勞動”(嚴格來講應該是相似勞動)
《Ruby元編程》中的一個例子:擦屁股專員的重複代碼
你是一個擦屁股專員,你接手一個老舊的裝置資訊系統,它維護者每台電腦的的裝置資訊與價格。這些資料來自一個古老的資料來源(DataSource)。所以你有一堆類似:
ds=DataSource.new
ds.get_mouse_info(workstation_id)
ds.get_mouse_price(workstation_id)
ds.get_keyboard_info(workstation_id)
ds.get_keyboard_price(workstation_id)
這樣的函數還有一打,電腦的每一個零組件都有這樣的一個函數。
為了配合新的報表系統,你需要返回一個封裝好的對象,這個對象針對每一個組件都有一個獨立的方法返回一個包含描述和價格的字串。很顯而易見,如果不想想什麼辦法的,未來的很長一段時間你都在拷貝、粘貼代碼的泥潭中掙紮。
每個方法都類似,每一個名為xxx的方法,首先通過ds.get_xxx_info獲得資訊,然後ds.get_xxx_price獲得價格,然後拼接字串...
解決方案:對於ruby,利用動態派發(send)\動態建立方法,甚至我們可以通過擷取內省來擷取datasource有哪些方法,得知有哪些組件,然後為這些組件建立方法。作為回報,你根本無需去建立\維護一個組件列表,如果有人在DS類中增加了一個新組件,你的Computer類將會自動的支援它。
---------------------------------------------------探討------------------------------------------------------
對於javascript程式員而言 ,依舊可以採用同樣的模式,內省+動態添加函數。
py這方面用的不多,就不說了,理論講應該是類似的。
java比較麻煩的是你不能動態添加方法,所以只能退而求其次,提供一個組件列表和一個String get_info(String component)函數。通過反射來擷取組件列表和調用函數。
c語言除了產生代碼以外,似乎別無他路。
ActiveRecord
用過的人都說好。它裡面的許多花樣與魔法都是利用元編程實現的。原則上說,目前能看到的 Meta Programming 有兩種,第一種專註於重新構成文法,即 DSL,Lisp/Scheme 屬於此類,這幾年的新科狀元 Ruby 也屬於這一派,甚至還有走得更遠但相對小眾的 REBOL Language
;第二種專註於在編譯期進行計算和指派以達到針對特定情境的自動代碼選擇最佳化,C++ 的模板屬於此類。C 的宏則兼有兩者,但能力都比較弱。據我見過的情況,學院派的比較認可前一種,後一種則借 C++ 的東風用得更廣泛。
問題引言裡的那句話,我認為是在說 C++,因為它強調了 do part of the work at compile time。但 Python 的 Meta-programming,我聞所未聞。Python 1.2 開始用到現在,我確實沒有聽說 Python 有什麼構建 DSL 的能力,更遑論所謂 Meta-programming。Python 社區的文化,也向來沒有在文法上玩花樣的傳統。於是大略查了查,它說的是應該是第三項東西:類結構動態調整,本質來說,就是反射(reflection)。參照 DeveloperWorks 上的這一篇:Metaclass programming in Python
,以及這一篇:Metaprogramming
。不難看出,這兩篇文章談的是完全另一個話題。因此談所謂 Lisp 和 Ruby 在 Meta-programming 上為什麼比 Python 做得好,在我看來是一個偽命題,因為兩者說的 Meta-programming 的內涵和外延都不一樣。
在實際工作中,無論哪一種 Meta-programming,我都持否定態度。但理由和前面朋友的觀點不同。我認為 Meta-programming 破壞了交流中的一項基本原則:同樣的程式設計語言,應當有同樣的交流習慣。過於隨意地構造 DSL,讓程式員之間的交流變得更加困難。這一方面的典型例子是 REBOL,幾種 DSL 文法差異明顯,重新學習的成本過高,也實際上助長了社區的分裂傾向。閑來無事,自己玩玩也就算了;放在工作裡,害人害己。
至於 Lisp/Scheme 家族的 Meta Programming,與其說是做得更好,倒不如說是文法結構過於簡單導致其怎麼變換都是長得一個樣子。關於這一點,不妨對比一下 REBOL,它構造出來的幾個 DSL 文法上沒有多少相似之處。不過,對我來說,Lisp/Scheme 這種文法簡單其實才是我喜歡的 Scheme:編譯器的設計者可以把更多的精力花在運行時和最佳化上。
還是那句話,我們寫程式是為瞭解決問題。抽象和表達方式是手段,不是目的。汲汲於文法上的花頭,是耍滑頭。我平時也不怎麼用這個東西,主要是我喜歡的語言meta programming的能力也不大。為什麼說LISP和Ruby的meta programming比Python好是因為,他們擁有“在編譯時間或者運行時重新讀取自己的代碼然後修改重新編譯的能力”,譬如說LISP的宏,譬如說Ruby的DSL的寫法,譬如說F#的computation expression,譬如說C#的linq和Expression,譬如說C++的模板(這個就比較牽強了),譬如說Haskell的monad。Python毛都沒有。
不過我覺得這個功能用處不大,沒事還是不要湊合了。不清楚元編程要解決什麼問題,但是可以說說Ruby元編程的應用:
1,Rails完全是成就於Ruby的元編程能力之上。
2,Ruby社區湧現出的各種優秀工具,無不借了元編程之力。
3,在Ruby中,元編程不特殊,它就是編程。
以上所說只是針對於說元編程沒大用的那些答案。Lisp的程式和資料都是基於List,所以只要能夠產生list結構,就能相當於能夠產生代碼,而Lisp的真正強大的宏機制,就是運行時的代碼展開與求值。
幾乎任何一個稍微大一點的Lisp程式多少都會有一些用來定義宏或者用宏編寫的代碼。
Ruby的元編程繼承自Lisp和Smalltalk,一方面可以通過eval來動態執行代碼,另外可以通過構造的語言閉包來開啟和關閉範圍。而且有非常簡潔和內省/反射機制來對程式的運行時狀態判斷,進而輔助代碼的產生。
C++的模板元編程的作用機制在於編譯期,通過模板對類型和資料的計算來進行展開產生代碼,所以才會有十分強大和通用的STL(標準模板庫)的出現。
Python在某種程度上還是可以進行元編程的(修改元類/資料?),只是靈活程度不夠高,所以少有人用,
Python 的哲學態度決定了這個社區會盡量選擇遠離元編程這東西。
確切的例子比如Ruby的ActiveRecord,參見Active Record Query Interface
和rails/activerecord
。
絕大多數的Ruby DSL都應用了Metaprogramming技巧的,另有書籍:Ruby元編程
。
如果深入研究,元編程的作用還是很大的。抽象能力比之於簡單的代碼提升了不止一個層級。這樣來說吧:
工業時代初期,是這樣的情況:
人生產機器,機器生產產品。
到了後期:
人生產機器,機器生產機器,機器生產產品。
顯然,機器生產機器是一個了不起的進步,這大大的解放了勞動力。
元編程就類似於機器生產機器,簡單說就是程式可以自己改變自己或者產生新的程式。這樣的當然大大的解放了程式員。
當然,事實上在元編程之前,程式員就發明了大量的協助工具輔助來協助自己編程,例如IDE什麼的。只是後來程式員覺得IDE啥的還是太弱了,就能尋找替換跳到引用套用模板啥的,要是程式可以自己生產自己該多好。
====================科普完畢,下面是不負責任的暢想=====================
事實上meta-programming一直都不是一個定義良好的概念,主要是meta(元)這個詞本身的含義就很模糊。
元一般被理解為更基本的,更基礎的。元編程就是比程式更基礎的的編程,也就是程式產生程式。從這個角度上來說,無論是C++的模板、C的宏、.NET的Emit和Compiler Provider、JavaScript的字串拼接和eval,甚至於什麼動網代碼產生器,都是程式產生程式。
但是一般而言,要稱得上元編程還是要滿足一定的條件,當然,由於元編程這個概念並非是定義良好,這些條件也只能算得上是什麼共識。這個語言的特性被A當作是元編程,而B不這麼認為是很正常的事情,下面列舉的這些條件,僅僅只是站在我的角度認為的:
1、具備元編程能力的語言,必須是可以產生同一程式設計語言代碼的語言,而不能是用一種語言產生另一種語言的程式。這就把代碼產生器排除在外了。
2、具備元編程能力的語言,元編程產生的程式碼,必須能夠通過詞法分析。這一條把C的宏排除在外和JavaScript的eval排除在外了。
……
可以看出來,到底算得上元編程,採用不同的限制條件可以得到不同的結果。當我們討論一個程式設計語言是不是可以元編程時,條件1基本是可以達成共識,條件2就各有各的看法。
話說元編程最初產生的時候,單純只是為了減輕程式員的負擔,C++的模板最初只是重載的一個延伸。由於強型別的緣故,C語言必須針對不同的類型編寫一大堆重複的代碼,重載解決了這些重複代碼的名稱汙染,而泛型則開始解決這些代碼的重複。但C++標委會那群傢伙顯然不會像Anders那樣止步於泛型,
高端大氣上檔次一直是C++努力不懈的追求,所以泛型就被搞成了模板,更進一步的,程式員也一起來Happy,就玩出了C++模板元編程。
先寫到這裡吧,元編程就是寫出可以產生代碼的代碼。有了這樣一層抽象,會讓你的程式更有擴充性,適應更多的未知的場合。java裡的反射,c++裡的STL的模板,還是ruby裡各種神奇的轉送 Proxy,這些都是元編程的應用。rails的成功構建於ruby強大的元編程,這也是rails為什麼沒有選擇其他語言的原因。