宏語言爲什麼不受歡迎

人類用計算機處理文本主要是依賴宏語言以及一些專用的文本編輯器。事實上,早期的文本編輯器只提供基本的文本編輯功能,而後藉助宏語言進行功能擴展。結果人類很快就發現,基於宏擴展的編輯器,功能越複雜,它的行爲就越詭異。因而,文本編輯器的擴展語言很快就被換成了當時的一種通用的動態類型的函數式編程語言——Lisp。實際上,這就是 Emacs 的前世與此生。程序員

研究編程語言設計的人,所追求的目標是,怎樣定義一套文法,使之既能使之對人類簡單又友好,且能準確無誤的轉譯爲另外一種語言。在專業作編程語言設計的人看來,宏語言是最弱的語言,由於它幾乎沒有什麼類型可言。類型越強的語言,每每越便於程序分析。編程

從宏處理器的角度來看,宏語言中只有兩種類型:文本與宏。宏展開的結果是文本,但宏自己也是文本,兩者的界限每每不是那麼明顯。在 M4 中,每每要藉助引號來區分宏與普通文本,而引號自己又有多是文本。類型如此貧弱,所以很容易在宏定義時引入一些並不顯而易見的錯誤,而這些錯誤沒法被其餘程序檢測。另外,用宏語言編寫的複雜程序一旦在運行時出現問題,就很難準肯定位問題所在,由於錯誤是在宏展開的結果中發現的,發現錯誤的時候,很難快速肯定它是哪一個宏的展開結果。編程語言

M4 最初是 C 語言之父 Dennis Ritchie 寫的,但他並無將 M4 做爲 C 語言的宏處理器,而是爲 C 語言設計了一種更爲輕巧、簡單的宏處理機制,顯然這是有意而爲之。編輯器

Eric Raymonad 在《Unix 編程藝術》一書中指出,功能越強大的宏處理器,越有可能帶來更糟糕的麻煩。TeX 引擎就是一種功能很是強大的宏處理器,可是要用它來作編程方面的事,也許定義兩個數的除法運算就須要上百行宏代碼,這種級別代碼複雜度致使 TeX 宏比 Perl 恐怖多了。有一些新的 TeX 引擎正在引入某種通用的編程語言來替換 TeX 的宏擴展機制。例如 LuaTeX,在一個重構的 TeX 引擎基礎上將 Lua 做爲擴展語言,也有嘗試將 Scheme 做爲 TeX 擴展語言的。函數式編程

雖然如今幾乎看不到宏語言的應用了,可是它依然默默的在工做着。幾乎全部的 Linux 系統都離不開 GNU Autotools 工具集。這個工具集就是基於 M4 語言構建的,其開發者將一些特定功能的 Shell 代碼封裝到一些 M4 宏中,而後由 GNU m4 負責將其展開爲 Shell 代碼。例如,下面這份簡單的 M4 宏代碼只有 9 行:函數

AC_INIT([m5], [0.1])
AC_CONFIG_AUX_DIR([build-aux])
AC_CONFIG_MACRO_DIR([m4])
AM_INIT_AUTOMAKE([foreign -Wall -Werror subdir-objects])
AC_PROG_CC
AM_PROG_CC_C_O
AC_CONFIG_HEADERS([config.h])
AC_CONFIG_FILES([Makefile])
AC_OUTPUT

可是它展開後的所得的 Shell 代碼卻長達 5000 餘行。而我在寫這 9 行代碼的時候,我幾乎徹底不懂 Shell 語言,可是我卻能理解這些 M4 宏的含義,由於它們只是軟件構建過程的一種抽象。工具

事實上,TeX 本來也是這樣。Donald Knuth 所開發的 TeX 系統,其排版原語只有 300 多個,可是經過 TeX 宏能夠將這些排版原語組合起來,從而完成更爲複雜的排版任務。對於這種任務,宏語言的運行效率要高於一種通用的編程語言。對於 Knuth 而言,這一決策是正確的,由於這樣的 TeX 徹底知足了他的需求。後來隨着排版任務的複雜化,宏的侷限性就日益的呈現了出來。若是始終堅持用宏的方式來擴展 TeX 的功能,進度是緩慢的,參與者的數量是逐步減小的,並且這一切都依賴於底層不能發生任何變化。這種系統早晚會變成恐龍的。Knuth 的 TeX 只支持 8 位字符,後來要讓它支持中文,Hacker 們不得不絞盡腦汁的在宏包的層面上去作工做,以致於如何讓 TeX 支持中文,對於中文用戶而言,長期以來一直是初學者遇到的第一個原本不該該是障礙的障礙。ui

濫用宏語言所提供的編程能力,所產生的問題每每要比它解決的問題更多。許多現代的編程語言已經再也不提供宏機制,C++ 雖然支持 C 語言宏,可是它幾乎不停的告誡程序員最好不要用宏,而是用 const 或內聯函數。設計

能夠用宏去薄層封裝那些繁瑣且須要屢次重複使用的代碼,可是原則上不要用宏去實現過於複雜的邏輯。code

我定義了一個 M4 宏 indent,它能夠將一個文本塊總體縮進必定距離。例如:

indent(`
foo bar
bar foo
foo bar
', `    ')

m4 的展開結果爲:

foo bar
    bar foo
    foo bar

這個宏的定義以下:

define(`NEW_LINE', `
')

define(`indent',
       `ifelse(eval(len(`$1') > 1),
               1,
           `ifelse(substr(`$1',0,1),
                   NEW_LINE,
               `format(`%s%s', NEW_LINE,`$2')`'indent(substr(`$1',1,eval(len(`$1')-1)), `$2')',
               `substr(`$1',0,1)`'indent(substr(`$1',1,eval(len(`$1')-1)), `$2')')',)')

這 10 行代碼,讓我寫了差很少半個下午,大部分時間都在與引號戰鬥。M4 宏若是出錯,首先應該是排查引號的錯誤。當我好不容易讓這幾行代碼可以成功運行以後,我發現它脆弱不堪,很容易崩潰。例如:

indent(`a(b)', `    ')

m4 試圖對其進行展開,而後它就會抱怨:

ERROR: end of file in argument list

由於在 indent 的遞歸展開過程當中,a(b) 中的 () 均會被 m4 錯認爲是某個宏的參數列表的括號。因爲 M4 不提供逃逸符,因此只能在 indent 的遞歸過程當中去檢測像 () 以及 , 這樣的符號,而後特殊處理。若是在上面這 10 行代碼的基礎上再增長處理這些特殊狀況的代碼……結果就是,說一句謊話,要用一百句謊話來掩蓋。即便 M4 提供逃逸符,也不怎麼會有人打算在代碼中爲頻繁出現的 與 `)' 之類的符號添加逃逸符的。

幸虧,GNU M4 提供了 patsubst 宏,使用它可實現 indent 但願實現的功能:

patsubst(`
abc
a(b)
c(a), e, f g', `
', `
    ')

GNU m4 的展開結果爲:

abc
    a(b)
    c(a), e, f g

因而可知,若是一個採用了宏擴展策略的系統,它所提供的『原語』級的實現有多麼的重要!

宏語言就像過去拿來包油條的舊報紙,後來人們以爲這樣很不衛生因而舊報紙就不能用來包油條了。不過,用塑料袋雖然衛生了我的,卻污染了環境。也許 Scheme 的衛生宏能夠用來包油條。

相關文章
相關標籤/搜索