對於 Scheme 語言的初學者而言,Scheme 的宏彷佛永遠是他們津津樂道的重要特性之一。譬如,我在上一章的結尾處說過,『也許不會再有比 Scheme 更高層次的編程語言了。雖然人類的大腦依然在源源不斷的構造着抽象之抽象的概念,可是 Scheme 自身能夠隨之進化——經過宏來定義新的語法』。這句話的背景彷佛很是宏偉,但我確信它是初學者的言論。若是稍微考察一下彙編語言,不難發現,彙編語言的宏也具有與 Scheme 宏的類似的特性。程序員
對於 Guile 而言,它所實現的 Scheme 標準以及 Guile 自家的一些模塊已經爲咱們定義了足夠用的宏了。在咱們的程序裏,幾乎不須要觸及宏。本章之因此要講述宏的基本知識,用意在於揭示宏是一種很簡單的編程範式,從而消除本身對宏的過分崇拜或恐懼之心。編程
不管是過程式編程,面向對象編程,泛型編程,函數式編程,仍是搞明白範疇論以後再編程,所要解決的基本問題是怎樣更有效的複用既有代碼。segmentfault
最原始的代碼複用方式是 Copy & Paste。這種最原始的代碼複用方式爲程序的 Bug 的繁衍作出了不可磨滅的貢獻,也許如今它還在兢兢業業的創造 Bug。不然,程序員們不會成天將 DRY(Do not Repeat Yourself)掛在嘴邊。安全
爲了消除代碼塊的 Copy & Paste 帶來的 Bug,有一些程序員開竅了,寫出來宏處理器。這樣,就能夠將重複使用的代碼塊單獨抽離出來,給它取個名字,而後將代碼塊中須要變化的部分用一些佔位符代替,並將佔位符做爲宏的形式參數。因而,就能夠將代碼塊轉化爲模板之類的東西。宏,本質上就是一種簡單可是自由的代碼模板,它對模板參數不作任何檢查。當一個宏被調用時,展開所得的代碼塊中的佔位符會被宏處理器替換爲這個宏所接受的參數。若是宏的參數有反作用,一般會在宏的展開結果中創造出難以察覺的 Bug。不過,這總比 Copy & Paste 安全多了,並且也高效多了。框架
C++ 的模板比宏高級了一些,但這是以大幅度犧牲宏的自由性而且大幅度增長編譯器的複雜性爲代價的。C++ 編譯器要求模板參數只能是數據類型——數據類型是永遠都沒有反作用的。與之相應,編譯器須要實現模板形式參數的替換、函數重載、重複模板實例消除等功能。C++ 有點像大禹,挖溝開河,折騰了許多年,終於將宏這種難以駕馭的洪水猛獸在必定程度上控制住了。C++ 的模板比宏要安全一些,並且項目開發效率也提高了一個臺階。編程語言
C++ 模板本質上依然是宏。儘管模板的參數是類型,可是 C++ 編譯器沒法肯定對參數是否正確。一旦 C++ 模板參數出錯,編譯器就會愚蠢的給出一堆不知所云的錯誤信息。換句話說,從 C++ 編譯器的角度看,模板的參數本質上只是文本,它沒法對這種文本進行邏輯上的判斷。發現這個問題的存在以後, C++ 社區又發展出一個新的概念,這個概念就叫作概念(Concept)。基於 Concept,能夠對模板參數的『行爲』進行約束。對於傳遞給模板的類型,C++ 編譯器會檢查這個類型是否擁有模板參數應該具備的行爲——有點像動態類型語言裏的鴨子類型(Duck Typing)。Concept 就是類型的類型。不知是何緣由,C++ 標準委員會三番五次的否決 Concept 的提案。函數式編程
Haskell提供了一種比模板更高級的代碼複用形式。C++ 的模板參數,對於 Haskell 而言就是函數簽名以及編譯器的自動類型推導。C++ 社區求之不得的類型的類型,對於 Haskell 而言就是類型類(TypeClass)。也就是說,Haskell 已經將以數據類型爲形參的宏的反作用完全的消除了。單從語言層面上來看,若是可以習慣 Haskell 不支持賦值運算這一特色,能夠將 Haskell 視爲更好的 C++。函數
這一切看上去都很美好,可是藉助宏來擴展自身的語法,這種需求彷佛被大部分編程語言的設計者刻意的忽視了。可能他們認爲,對語言自身進行擴展,那是語言標準委員會以及編譯器開發者的任務,而不是軟件開發者的任務。不少人認爲,縱容軟件開發者對語言進行擴展會形成語言的分裂。他們會說,不妨統計一下,這個世界上有多少個版本的 Lisp 與 Scheme 語言的實現。對宏進行弱化,能解決語言分裂的問題麼?我以爲這只是迴避問題的辦法,而不是解決問題的辦法。能夠想想,有些庫自稱是框架,它們所作的事情是否是企圖基於類或高階函數對語言自己進行擴展?若是可以很面向特定領域,爲語言增長一些擴展,使之成爲領域專用語言,這豈不是比框架要好得多得多?學習
宏的真正用武之地就在於操縱語言自身的語法,爲某些特定的問題領域定製更易於理解與應用的語法。所謂的元編程,其用意大抵也是如此。
譬如 C 語言的宏,雖然其功能極弱——只能展開一次,可是依然能爲 C 擴展出好用一些的語法。例如 GObject 庫爲基於 C 語言提供面向對象提供了有力支持,下面是它的一個空的類的定義示例:
typedef struct _MyObject{ GObject parent_instance; } MyObject; typedef struct _MyObjectClass { GObjectClass parent_class; } MyObjectClass; G_DEFINE_TYPE(MyObject, my_object, G_TYPE_OBJECT);
它等效於下面的 C++ 代碼:
class MyObject { };
G_DEFINE_TYPE
能夠將一個結構體類型註冊到 GObject 實現的動態類型系統,從而產生一個相似於 C++ 的『類』的類型。
C 編譯器展開 G_DEFINE_TYPE
宏後,大體能夠獲得如下 C 代碼:
static void my_object_init(MyObject * self); static void my_object_class_init(MyObjectClass * klass); static gpointer my_object_parent_class = ((void *) 0); static gint MyObject_private_offset; static void my_object_class_intern_init(gpointer klass) { my_object_parent_class = g_type_class_peek_parent(klass); if (MyObject_private_offset != 0) g_type_class_adjust_private_offset(klass, &MyObject_private_offset); my_object_class_init((MyObjectClass *) klass); } __attribute__ ((__unused__)) static inline gpointer my_object_get_instance_private(const MyObject * self) { return (((gpointer) ((guint8 *) (self) + (glong) (MyObject_private_offset)))); } GType my_object_get_type(void) { static volatile gsize g_define_type_id__volatile = 0; if (g_once_init_enter(&g_define_type_id__volatile)) { GType g_define_type_id = g_type_register_static_simple(((GType) ((20) << (2))), g_intern_static_string("MyObject"), sizeof(MyObjectClass), (GClassInitFunc) my_object_class_intern_init, sizeof(MyObject), (GInstanceInitFunc) my_object_init, (GTypeFlags) 0); } return g_define_type_id__volatile; };
雖然使用 GObject 來編寫面向對象的 C 程序要比 C++ 繁瑣一些,可是學習成本卻低了許多。若是 C 的宏可以像 m4 那樣強,在語言層面基於宏精心擴展,在語言層面支持面向對象編程範式並不是難事。我曾經用 GNU m4 實現過一個簡單的單層匿名函數機制,偶爾能夠嚇到一些人。
include(`c-closure.m4')dnl #include <stdio.h> _C_CORE int main(void) { char *str_array[5] = {"fetch", "foo", "foobar", "sin", "atan"}; @(`char *', `foo', `"foo"'); qsort(str_array, 5, sizeof(char *), _LAMBDA(`const void *p1, const void *p2', `int', `int d1 = dist(* (char * const *) p1, &(`foo')); int d2 = dist(* (char * const *) p2, &(`foo')); if (d1 < d2) { return -1; } else if (d1 == d2) { return 0; } else { return 1; }')); for (int i = 1; i < 5; i++) { printf("%s\n", str_array[i]); } exit(EXIT_SUCCESS); }
從如今開始,就應該牢記:宏是用來擴展語法的,不要將它做爲函數來用。Scheme R5RS 標準中強調了這一點,而且將宏定義語法設置爲如下格式:
(define-syntax macro <syntax transformer>)
下面這段代碼爲 Guile 定義了相似 C 語言的 if .. else if ... else
語法:
(define-syntax if' (syntax-rules () ((if' e result) (cond (e result))) ((if' e result else' <result>) (cond (e result) (else <result>))) ((if' e result else' if' <e> ...) (cond (e result) (else (if' <e> ...))))))
因爲 Guile 有本身的 if
語法,因此我在 if
後面加了個單引號以示區別。
if'
宏的用法以下:
(if' #f (display "1") else' if' #f (display "2") else' if' #f (display "3") else' (display "nothing"))
因爲我如今只是 Guile 的初學者,因此我並不保證 if'
的定義是否正確。爲了寫出這個宏,我大概折騰了半個下午。不過,這個宏的意圖很簡單,它可讓咱們在編寫條件分支時少寫一些括號。
這個 if'
宏是由三條語法規則——syntax-rules
塊中的三個表達式構成的。來看第一條語法規則:
((if' e result) (cond (e result)))
這條語法規則描述的是,若是遇到像 (if' e result)
這樣的表達式,Guile 便將其轉換爲 (cond (e result)))
。表達式 (if' e result)
被稱爲模式,它表示的是含有三個元素的列表。也就是說,凡是含有三個元素的列表,都是 (if' e result)
模式,這樣的列表有無數個,可是其中大部分不是咱們須要的。由於若是要使用 if'
宏,這個列表的第一個元素應該是 if'
符號,其他兩個元素應該知足 cond
的要求。下面這些表達式都符合 (if' e result)
模式:
(if' #t (display "Hello world!")) (if' 2 3) (if' (< 2 3) #t) (if' (display "Hello") (display " world!"))
在上述表達式中,咱們使用 if'
宏,本質上是讓 (if' e result)
這個模式與上述這些表達式進行匹配,這個過程被稱爲模式匹配。一旦模式匹配成功,Guile 會將模式中的各個符號便會與其匹配的子表達式綁定起來。在語法規則中,位於模式表達式右側的那個表達式稱爲模板。每一個模式表達式對應着一個模板。模板經過模式中的符號來引用這些符號所綁定的子表達式。能夠將模式理解爲宏的名字及其參數的聲明,將模板視爲宏的定義。
Scheme 採用語法規則的方式來定義宏,好處是能夠定義多個同名的宏。用面向對象的術語來講,就是 Scheme 宏是多態的。if'
的其餘兩個版本是:
((if' e result else' <result>) (cond (e result) (else <result>))) ((if' e result else' if' <e> ...) (cond (e result) (else (if' <e> ...))))
須要注意,Scheme 宏是容許遞歸的。(if' e result else' if' <e> ...)
模式所對應的模板中含有 if'
的遞歸。由於有了這個遞歸,因此 if'
宏能夠支持多條 else' if'
從句。
按照 if'
宏的第二條語法規則中的 (if' e result else' <result>)
模式,能夠像下面這樣使用 if'
:
(if' #f #f else' #t)
可是,下面這個表達式:
(if' #f #f i-am-guest-actor #t)
它也符合 (if' e result else' <result>)
模式,由於 i-am-guest-actor
會被綁定到 else'
符號,而 else'
符號在模板中並無用到,因此它綁定了什麼是無所謂的。可是,咱們顯然是但願 else'
有意義。
針對此類問題,Scheme 爲語法規則提供了關鍵字支持。只需將上一節給出的 if'
宏的定義修改成:
(define-syntax if' (syntax-rules (else' if') ((if' e result) (cond (e result))) ((if' e result else' <result>) (cond (e result) (else <result>))) ((if' e result else' if' <e> ...) (cond (e result) (else (if' <e> ...))))))
這樣,模式中的 if'
與 else'
便都被設定爲關鍵字。在使用 if'
宏時,若是再隨便用別的符號來代替 else'
或 else' if'
,那麼 Guile 便會報錯,說找不到匹配模式。
let
let
是個頗有用的語法,它能夠在函數內開闢一個包含一些變量與一個計算過程的『局部環境』。若是沒有 let
,就只能經過函數的嵌套來作這件事,結果會致使代碼有些扭曲。
假設存在一個數學函數(取自 SICP 1.3.2 節):
$$f(x,y)=x(1+xy)^2+y(1-y)+(1+xy)(1-y)$$
如今爲它寫編一個 Guile 函數。衆所周知,Guile 的前綴表達式在表現複雜的代數運算式時會失於繁瑣。例如:
(define (f x y) (+ (* x (* (+ 1 (* x y)) (+ 1 (* x y)))) (* y (- 1 y)) (* (+ 1 (* x y)) (- 1 y))))
這麼多年,是哪些人沒良心的吹噓 Scheme 簡單又優美呢?
若是將上述的數學函數寫爲 $f(x,y)=xa^2 + yb + ab$,其中 $a$ 與 $b$ 分別爲 $(1+xy)$ 與 $(1-y)$,那麼就能夠將 Guile 代碼簡化爲:
(define (f x y) (+ (* x (* a a)) (* y b) (* a b)))
可是不可避免的面臨一個問題,在函數 f
內,如何製造兩個局部變量 a
與 b
呢?能夠像下面這樣來作:
(define (f x y) ((lambda (a b) (+ (* x (* a a)) (* y b) (* a b))) (+ 1 (* x y)) (- 1 y)))
就是在函數 f
內部定義一個匿名函數,並應用它。在應用這個匿名函數時,Guile 會將其形參 a
與 b
便會分別與實參 (+ 1 (* x y))
與 (- 1 y)
綁定起來。
用上面這樣的方式寫代碼,是否是世界觀有些扭曲?不過,咱們能夠將這種扭曲的代碼用宏封裝起來,造成 let
語法。事實上,在 Guile 中,let
自己就是用宏實現的語法:
(define-syntax let (syntax-rules () ((let ((name val) ...) body1 body2 ...) ((lambda (name ...) body1 body2 ...) val ...))))
有了 let
,就能夠將上面那個函數寫爲:
(define (f x y) (let ((a (+ 1 (* x y))) (b (- 1 y))) (+ (* x (* a a)) (* y b) (* a b))))
下面是 C 的一個宏的定義:
#define SWAP(x, y, type) {type c = x; x = y; y = c;}
這個宏用於交換兩個同類型變量的值,其用例以下:
int a = 3, b = 7; SWAP(a, b, int);
結果 a
的值會變爲 7
,b
的值會變爲 3
,也就是說 a
與 b
的值被 SWAP
宏交換了。
看上去,SWAP
這個宏沒有什麼問題,可是它有着一種匪夷所思的反作用。看下面這個例子:
int b = 3, c = 7; SWAP(b, c, int);
若是不去看 SWAP
的定義,咱們會想固然的認爲 b
與 c
的值會被 SWAP
交換,但事實上兩者的值不會被交換。由於 C 預處理器會將上述代碼處理爲:
int b = 3, c = 7; {int c = b; b = c; c = c;}
{ ... }
裏的 c
是一個局部變量,對它進行任何修改都不會影響 { ... }
外部的 c
。
若是將 SWAP
的定義修改成
#define SWAP(x, y, type) {type _______c = x; x = y; y = ______c;}
這樣能夠大部分狀況下能夠避免宏定義內部的臨時變量與宏調用環境中的變量重名所帶來的問題。不過,無人能保證不會有人向 SWAP
宏傳遞一個名字是 _______c
的參數。
這個故事告訴咱們,在使用 C 的宏時,最好對其定義有所瞭解。
如今來看 Guile 版本的 SWAP
宏的定義及用例:
(define-syntax swap (syntax-rules () ((swap x y) (let ((c y)) (set! y x) (set! x c))))) (define b 2) (define c 9) (swap b c)
結果 b
與 c
的值互相交換了。
在 syntax
環境中所用的臨時變量,Guile 會自動對臨時變量進行重命名,而且保證這個名字從未在宏的定義以外使用過。單憑這一功能,Scheme 的宏就能夠藐視其餘語言的宏了。
Scheme 的宏之因此能像耍魔術同樣的製造出一些有用的語法,緣由在於 Scheme 語法形式上的統一,這種形式就是所謂的 S-表達式。不管傳遞給宏的文本有多麼複雜,只要它在形式上是 S-表達式,那麼在宏的內部即可以對這種文本從新組織。
用編譯原理中的術語來講,Scheme 的宏能夠將其接受的一棵語法樹轉換爲其餘形式的語法樹。所謂語法樹,是目前任何一種比彙編語言高級的編程語言經編譯器/解釋器做語法分析後所得結果的抽象表示。Scheme 代碼自己就是語法樹,Scheme 解釋器無需再對其進行語法分析,在這個層面上,經過宏能夠自由的對語法樹進行從新組織。只有運行於語法分析樹層面上的宏機制才能具備 Scheme 宏這樣的能力。
C/C++ 所提供的宏機制,運行於編譯或解釋階段以前的預處理階段,它的工做只是遵循特定規則的文本替換,預處理器並不知道本身所處理的文本的邏輯結構。此類宏機制相似於外科手術中的器官移植,而基於 S-表達式的 Scheme 宏則相似於基因重組。
本章的導言中說『若是稍微考察一下彙編語言,不難發現,彙編語言的宏也具有與 Scheme 宏的類似的特性』。雖然彙編語言的宏機制本質上也是文本替換,可是彙編語言向機器語言的轉換是不須要語法分析的,而是彙編指令向機器指令的直接映射。所以,彙編語言的宏也是在作『基因重組』的工做。
像 TeX 與 m4 這樣的軟件,它們提供的宏功能比 C/C++ 宏機制更強大,但本質上依然是遵循特定規則的文本替換。因爲文本自身就是它們操做的對象,因此它們的宏本質上是將文本視爲『基因』進行重組。從這個角度來看,能夠說它們的宏也具有與 Scheme 宏類似的特性。例如,m4 經常使用的宏基於一組基本的宏構建而成。Autoconf 是基於 m4 構建的一種領域專用語言。TeX 不可勝數的宏包,每一個宏包都是一種領域專用語言。不過,得之於宏,也失之於宏,譬如 TeX 雖然極爲擅長展自身語法,可是它在編程方面卻很是貧弱。正是因爲這個緣故,因此纔會出現 LuaTeX 項目,該項目嘗試將 Lua 解釋器嵌入 TeX 宏處理器,從而以 Lua 語言補了 TeX 在編程方面的不足。
Haskell 爲軟件開發者留了一個能夠訪問其語法分析結果的接口,所以 Haskell 也具備相似於 Scheme 宏的能力,只不過 Haskell 沒有在語法層面提供宏。Haskell 愛好者們認爲,Haskell 提供了惰性計算、準引用以及 GHC 的 API,基於這些機制,Scheme 宏能作到的事,Haskell 也能作到。