Meta Programming 主要解決什麼了問題?

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可以做到非常自然和直觀,因而其元編程更自然,更漂亮。


批量生成各種代碼(類,方法,測試等等),減少編程時的重複操作。各種DSL,code generator,scalfolding,project bootstraping都受益於meta programming。


Ruby元編程這本書的引言就講的其實挺清楚的。
我到是建議你看完這本書,相信你能夠理解的,裡面例子還不錯,這本書我還沒看完。我現在手邊就是。
順便說說Lisp,大家都知道「代碼及數據」這回事,其實就是這樣子一回事。元編程的定義就是寫出編寫代碼的代碼,lisp裡面quote可以阻止求值,到需要使用的時候用eval就行了。然後在運行的時候操作自己的代碼也是他另一個含義。可以call 它叫動態元編程,以此區分代碼生成器以及編譯器方式的靜態元編程。


把 template&<...&> 看作一個函數,那麼模板就是一種可以把類型作為參數,並返回類型的函數。模板的本質就是對類型的運算。


編程(programming)是對於某類問題解決方法的抽象。
而元編程(meta programming)則是對於某類解決方法的抽象。
也就是說,元編程是對於編程的抽象。
至於說到DSL這部分我不敢苟同,誠然,ruby圈的DSL有泛濫的趨勢,為了DSL而創造DSL、為了炫技而DSL,但是這些不是重點。DSL的出現是高度抽象的必然結果,脫離了抽象的DSL自然會死亡。
舉幾個栗子:
ORM
我有現在有N個實體類,我需要把這些實體類持久化儲存。所以我寫了N個DAO。後來發現,DAO這些東西好像很無聊嘛,無非就是那麼幾步,根據實體類名稱創建表,根據屬性創建資料庫欄位,然後增刪改查。(ORM)
為了解決這些「相似的重複勞動」,我們需要某種機制,能夠將我的一個"meta program"轉化成具體的某個program。這就是抽象層次的差別。
ORM的基本步驟:根據實體類類名確定資料庫表名,根據實體類屬性確定資料庫欄位,統一的insertdeletefindupdate 方法,根據屬性與資料庫欄位的對應關係完成數據恢復與保存。在java中,我們通過反射拿到類名和資料庫欄位,利用泛型+集成得到insertdeletefindupdate以及各種公共方法。結果就是你只需要簡單繼承一個父類,就可以完成DAO的操作。 (GitHub - satyan/sugar: Insanely easy way to work with Android Database. 這裡有一個例子)
語法分析與詞法分析
語法分析與詞法也是一類具有很大相似性但是又有區別的事情。一個典型的例子就是lexyacc(flexbison), 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
用過的人都說好。它裡面的許多花樣與魔法都是利用元編程實現的。


不清楚元編程要解決什麼問題,但是可以說說Ruby元編程的應用:

1,Rails完全是成就於Ruby的元編程能力之上。
2,Ruby社區湧現出的各種優秀工具,無不借了元編程之力。
3,在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++模板元編程。

先寫到這裡吧,


元編程實現的抽象層次更高,提供了一種在運行期擴展自身的能力。。
如果,你需要定義100個方法。

1. 你可以編寫100個方法
2. 可以編寫一個方法生成100個方法。
3. 實現一個動態方法來接管某些調用請求。

在第二種情況下,這100個方法,在你編寫的源碼裡面是找不到的,當程序運行時候,它動態裝載方法到內存里,是真實存在的。 第三種情況下,即便是內存里也找不到你方法的完整定義,它是一個幽靈。在ruby中,它將被method_missing接管。

在某些情況下,你不知道你需要定義的方法的數量是未知的,如果你不想改變編程策略(例如換個解決方案的話),你需要藉助元編程。

或者,根本就沒有元編程,從來都只有編程而已。


在語言中創造語言, 而不需要去實現或擴展Compiler
這裡有簡單的例子
Nim是一種語法混合Pascal跟Python的語言, 有Lisp等級的元編程
Nim Programming Language
Nim函式庫jnim的範例就創造了一種語言, 用於跟Java的函式庫串接, 這新語言讓程序只需要宣告, 他就會在看不見的地方自動補上其他必要的程式碼
yglukhov/jnim · GitHub
要是用metaprogramming不夠強的語言, 恐怕每個函式都得去實作呼叫JVM的對應函數, 而不會像是下面一樣僅僅宣告就能使用

import jnim

jnimport:
# Import a couple of classes
import java.lang.System
import java.io.PrintStream

# Import static property declaration
proc `out`(s: typedesc[System]): PrintStream {.property.}

# Import method declaration
proc println(s: PrintStream, str: string)

# Prepare the Java environment. In this case we start a new VM.
# Not needed if you are already in JNI context.
let jvm = newJavaVM()

# Call! :)
System.`out`.println("This string is printed with System.out.println!")


原則上說,目前能看到的 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 其實很多語言都有,比如 C。

你一定很驚訝。因為我說的是字面的 meta-programming。平常人認為的 meta-programming,實際上是 control flow abstraction。也就是在語言中創造新的 control flow。

其實還有一種 abstraction - data type abstraction,我們每天都用。用 struct/class 創建新的 type。

很多所謂 CS 就不平衡了,理論家最講平衡的。既然 data 可以 abstraction,control-flow 也要 abstraction。所以主要解決了理論優美上的平衡問題


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元編程 。

如果深入研究,元編程的作用還是很大的。抽象能力比之於簡單的代碼提升了不止一個層級。


舉個栗子,django的ORM用的就是meta programing。
開發者可以直接把一個抽象的對象映射到資料庫而無需關注細節,這種忽略甚至可以讓一個對資料庫完全沒有概念的程序員直接使用資料庫。


我平時也不怎麼用這個東西,主要是我喜歡的語言meta programming的能力也不大。為什麼說LISP和Ruby的meta programming比Python好是因為,他們擁有「在編譯時或者運行時重新讀取自己的代碼然後修改重新編譯的能力」,譬如說LISP的宏,譬如說Ruby的DSL的寫法,譬如說F#的computation expression,譬如說C#的linq和Expression&,譬如說C++的模板(這個就比較牽強了),譬如說Haskell的monad。Python毛都沒有。

不過我覺得這個功能用處不大,沒事還是不要湊合了。


編程的本質就是抽象,meta programming 的抽象層次最高。


好像回答都有些偏科班視角或者概念定義(那個說report的倒是比較實際),我來個接地氣的民工答案吧。

事實上元編程和其他所有抽象工具(如繼承,mixin)最本質的區別是元編程的抽象是不依賴於特定名字的。
大部分教學代碼的抽象都不至於會有「不可依賴特定名字」這個要求,但真的系統複雜了,名字還是會衝突,這種時候,如果真的是潔癖和強迫症的話,就會恨不得來元編程。

例如,你想要抽象「一個數據有很多子條目」,你可以用傳統的抽象方式。

class 主數據項{
 private 集合 data;
 集合&<子數據類&> getChildren(){
  return data;
 }
}

class 子數據項{
 主數據項 getOwner();
}


但現在你拿到的需求是這樣的:你是一個遊戲玩家,你擁有許多技能,也擁有許多道具,那麼你怎麼貫徹之前寫好的主數據項/子數據項抽象?
class 玩家 extends 主數據項{}
class 技能 extends 子數據項{}
class 道具 extends 子數據項{}

這時候問題就會出現,無論是技能還是道具,都被塞到玩家從主數據項類繼承的data變數里去了。我想知道「玩家身上有多少道具呢」而調用的getChildren().getCount()就是完全不準確的。我枚舉他身上有哪些道具賣給道具商人的時候,他就可能會不小心把「廬山升龍霸」賣掉。

因為在這個抽象里,主數據項類里的集合名稱必須是「data」,包括介面也必須是"getChildren",名稱鎖死導致針對「道具」和「技能」的兩個不同抽象的實現失敗了。

就不用說java的單根繼承了,即使是ruby的mixin,還是C++的多繼承都解決不好這個問題,各種依賴具體名字的抽象方案對這種需求理所當然都是無能為力的。

元編程解決這種問題是最合適的,不管是用宏,還是用ruby的eval做代碼生成,最終我們可以指望達到這樣一個效果:
class 玩家{
 has_many :技能
 has_many :道具
}
class 技能{
 has_owner :玩家
}
class 道具{
 has_owner :玩家
}

然後就可以歡快的

玩家.技能List.add
玩家.道具List.add
玩家.技能List.count
玩家.道具List.count
特別重的道具.玩家.說話("好累")

了。正因為名稱沒綁死,所以抽象才能成功。
這也正是rails的數據模型裡面大量使用元編程的必要性。

當然以上這個需求不使用元編程也是可以做的,你看我們回到笨笨的Java:
class 玩家{
 主數據項 技能List;
 主數據項 道具List;
}
然後我也可以
玩家.技能List.getChildren() 湊合著用。
這個就是把"繼承"改為"包含"的設計原則,確實管用。
但,代碼變得無比啰嗦且不論,類型界定,唯一性識別,以及主對象對子對象的生命周期管理和事件傳遞就變成了一個巨大的負擔。
比如,主數據項的刪除,必須要考察子數據項是否刪完,否則要提示出錯。

void 刪除數據(數據){
 if (數據 instance_of 主數據項){
  if (((主數據項)數據).getChildren().count!=0){
   for(Object 子數據 : ((主數據項)數據).getChildren()){
    刪除數據(子數據);
   }
  }
 }
}

那麼一個你不認識的程序員想當然的調用你寫好的代碼
數據服務.刪除數據(玩家);
的時候,因為你的玩家實現是「包含」而非「繼承」了「主數據項」,因此相關的判斷和子項刪除會被完全跳過。

於是一大堆道具和技能就這樣留在硬碟上,永遠沒人去刪除了,隨著系統運行的時間越來越長,這些沒有及時刪除的道具和技能數據撐爆了硬碟,你的年終獎金就這樣灰走了~

為了預防這個可怕的後果,你被迫寫了
class 玩家{
 /**
 **注釋:不要用標準的刪除操作刪我!一定要調用這個函數!
 **/
 public 定製刪除函數(){
  數據服務.刪除數據(this.道具List);
  數據服務.刪除數據(this.技能List);
  數據服務.刪除數據(this);
 }
}

很醜陋吧?
如無必要,勿增實體。而這個技能List和道具List就是因為語言的抽象能力達不到需求的要求,而被迫增加的實體。為了這些實體不出簍子,你還必須寫一大堆保姆代碼來喂吃喂喝洗澡換尿布之類的。

說實話我不喜歡元編程,對邏輯梳理和調試改錯都帶來了許多麻煩,很多場合里也根本沒法指望IDE支持。但程序員的第一戒律「Don"t repeat yourself」在上,類似的場景里我永遠會有使用宏或者ruby eval的衝動。
我不像反面向對象的激進主義者那樣這麼討厭繼承,我希望能有支持「非特定名稱繼承」的語言結構能出來。


Meta-programming的一個應用是,通過override eval機制,可以動態實現AOP而無需使用runtime的instrument API(也許本來就沒提供)、hooks,也無須靜態weave client code


元編程就是寫出可以產生代碼的代碼。有了這樣一層抽象,會讓你的程序更有擴展性,適應更多的未知的場合。java里的反射,c++里的STL的模板,還是ruby里各種神奇的轉發代理,這些都是元編程的應用。rails的成功構建於ruby強大的元編程,這也是rails為什麼沒有選擇其他語言的原因。


Meta Programming 主要解決什麼了問題,我覺得這個問法不好,應該說 Meta Programming 是怎麼產生的。我更願意把元編程理解為一種解決方法的思路,或者看待編程問題的一種方式。這種方式就是過程分形的一種體現,所謂過程的分形,就是解決問題的整個過程,和解決問題中某個步驟,在形式上是一致的。如同幾何圖案的分形,或者組織結構的分形一樣。

編程的整個過程就是把要解決的問題轉換為程序的過程。而元編程就是編程的編程,用程序將要解決的問題轉換為程序。這是一種很自然的想法。

其實整個計算機的發展史就是這樣。從機械設備到通用計算機,我們將解決問題的方法(機械設備就是硬編碼的方法)變成機器指令,相對於計算機,這些機器指令就是它的數據。然後我們創建了編程語言/編譯器/解釋器,用來將高級語言/抽象的問題藐視轉換為機器指令,那麼高級語言就是這些編程語言/編譯器/解釋器的數據,而此時機器語言是硬編碼的,而高級語言是數據。我們再編寫各種應用軟體,此時高級語言本身又是硬編碼,而我們的數據成了數據……在每個層次上看,這個過程本身是一樣的,這就是過程的分形。

所以用程序處理程序,和把程序當成數據,是元編程的本質,而這種思想是很自然出現的。


舉個meta programming 的例子:jasper report ,就是一套report的生成和展示工具,可以用來所見即所得布署XML格式的report模板,供顯示和生成report.
沒有這套engine時,毎個report都要寫很多代碼,累死了,維護也極不方便。

最近我做了一套保險公司的保費計算引擎,各種計算公式很繁瑣又多樣,每一種產品還有不同。我就把它們保存在excel里並生成XML,在程序里根據xml來計算結果,效果很好。


如果說普通的編程活動是面向業務的,那麼元編程就是面向純粹的一行行的代碼的。元編程,就是將代碼作為元數據來處理。這方面的傑出代表是lisp的宏系統。你完成一樣東西,可以從頭到尾純手工完成,但是元編程是讓你關注於發明一系列工具解脫手工,提高生產效率,有了工具後你直接站在更高層的角度來審視問題,更簡潔的將業務描述。


泛函-函數
模板-類
更高一級的抽象


個人理解,元編程主要還是用於解決抽象程度不夠的問題。 在沒有或者元編程能力比較弱的語言中,由於語言支持的抽象能力不夠,因此邏輯類似,但細節上有差異的代碼,需要重複多次,典型的就是靜態語言。 C++為了解決這個問題引入了模板機制,這在部分程度上解決了問題,但實在是一種非常醜陋的方式,這是由於很多問題的本質,需要動態的加以解決,而C++ 的模板需要在編譯的時候就解決,這引出了非常多魔幻般的技巧。這對一般普通群眾來說,基本上屬於天書。

相對而言,支持元編程的語言,例如lisp ruby,本質上並沒有什麼編譯和運行的說法,這類語言具備強大的運行環境支持,編譯和運行是合一的。程序在運行時,又能夠動態的修改運行環境本身,從而很容易實現了所謂元編程,其實就是對程序運行環境的修改。

無論元編程還是dsl最終的目標還是為了提高抽象的程度,從而完成業務邏輯上的簡化。這對最終使用者有明顯的好處。


推薦閱讀:

有哪些老程序員都知道對新手很有用的經驗?
為什麼 Go 語言把類型放在後面?
新手關於如何看編程經典書的一些疑惑?
網上常能見到的一段 JS 隨機數生成演算法如下,為什麼用 9301, 49297, 233280 這三個數字做基數?
大學學的不是喜歡的專業怎麼辦?

TAG:程序員 | Ruby | Python | 編程 | Lisp |