2005 年 11 月 21 日 目前應(yīng)用最廣泛的技術(shù)之一是編寫生成其他程序或部分程序的程序。因此十分有必要學(xué)習(xí)為什么要采用元編程,以及元編程都有哪些組件(文本宏語言,專用代碼生成器)。在本文中,您將學(xué)習(xí)到如何構(gòu)建一個代碼生成器,并詳細了解如何使用 Scheme 編寫對語言敏感的宏。 用來生成代碼的程序有時被稱為 元程序(metaprogram);編寫這種程序就稱為 元編程(metaprogramming)。編寫這種輸出代碼的程序可以有無數(shù)的應(yīng)用。 本文將介紹為什么會考慮進行元編程,并介紹這種技術(shù)的一些組件 —— 我們將深入介紹文本宏語言(textual macro language),了解專用的代碼生成器,并討論如何構(gòu)建這些工具,最后研究如何使用 Scheme 編寫對語言敏感的宏。 元編程的不同用法 首先,可以編寫一些程序來提前生成一些數(shù)據(jù)供運行時使用。例如,如果您正在開發(fā)一個游戲,并且希望使用一個所有 8 位整數(shù)的正弦值的查詢表,既可以每次都執(zhí)行正弦計算的操作,也可以讓程序在啟動時構(gòu)建這樣的一張表在運行時使用,或者編寫一個程序在編譯之前為這個表生成定制代碼。盡管對于少量的數(shù)據(jù)來說在運行時構(gòu)建這張表是可能的,但是有些任務(wù)則可能會使得程序啟動非常緩慢。在這種情況中,編寫一個程序來構(gòu)建一張靜態(tài)表通常是最好的解決方案。 其次,如果您有一個很大的應(yīng)用程序,這個程序有很多函數(shù)都包括了很多樣板文件,那么就可以創(chuàng)建一個小型的語言,它可以生成這些樣板代碼,讓您可以只實現(xiàn)重要的部分?,F(xiàn)在,如果可以,最好是能夠?qū)⑦@些樣板部分抽象成一個函數(shù)。但是通常來說,這些樣板代碼并不會如此精美??赡苊總€實例中都需要聲明一些變量,可能需要注冊錯誤處理程序,或者有一些樣板文件必須在某些情況中插入一些代碼。所有這些都使得簡單的函數(shù)調(diào)用是不可能的。在這種情況中,通常創(chuàng)建一個小型的語言來更簡單地利用樣板文件的代碼。這種小型的語言可以在編譯之前被轉(zhuǎn)換成普通的源代碼語言。 最后,有很多編程語言都可以編寫非常復(fù)雜的語句來真正實現(xiàn)一些功能。代碼生成程序可以對這種語句進行簡化,并節(jié)省很多輸入的工作,這可以防止大量的輸入錯誤,因為減少了很多輸入錯誤內(nèi)容的機會。 作為語言可以有很多特性,代碼生成程序就不需要這么多了。一種語言中的標(biāo)準特性在另外一種語言中可能只能通過代碼生成程序?qū)崿F(xiàn)。然而,語言設(shè)計不充分并不是需要代碼生成程序的唯一原因。維護簡單也是一個原因。
文本宏語言基礎(chǔ) 代碼生成程序允許您開發(fā)并使用小型的、領(lǐng)域特有的語言,這樣比直接在目標(biāo)語言中開發(fā)這種功能更容易編寫和維護。 用來創(chuàng)建領(lǐng)域特有語言的工具通常稱為 宏語言(macro language)。本文介紹了幾種宏語言的方法,并介紹了如何改進代碼。 C 預(yù)處理器(CPP) 首先讓我們來看一下涉及文本宏編程的元編程。文本宏(textual macro) 是可以直接影響編程語言中的文本的宏,它們并不需要了解或處理語言的意義。兩個最廣泛使用的文本宏系統(tǒng)是 C 預(yù)處理器和 M4 宏處理器。 如果您曾經(jīng)使用 C 進行過編程,那么可能處理過 C 語言中的 #define 宏。文本宏的擴展雖然不甚理想,但在很多沒有更好的代碼生成能力的語言中,這是用來進行基本元編程的一種簡單方法。清單 1 給出了一個 #define 宏的例子: 清單 1. 用來交換兩個值的宏
#define SWAP(a, b, type) { type __tmp_c; c = b; b = a; a = c; }
|
這個宏可以交換兩個給定類型的值。由于幾個原因,這最好是作為一個宏來實現(xiàn): - 對于這種簡單的操作來說,函數(shù)調(diào)用的開銷太大。
- 需要向函數(shù)傳遞變量的地址而不是變量的值。(這并不是很大的問題,但是傳遞值會使函數(shù)調(diào)用比較混亂,并且編輯器就無法將這些值保存在寄存器中了。)
- 對于每種需要交換的類型來說,都需要定義一個不同的函數(shù)。
清單 2 給出了一個使用宏的例子: 清單 2. SWAP 宏的使用
#define SWAP(a, b, type) { type __tmp_c; c = b; b = a; a = c; }int main(){ int a = 3; int b = 5; printf("a is %d and b is %d\n", a, b); SWAP(a, b, int); printf("a is now %d and b is now %d\n", a, b); return 0;}
|
當(dāng)運行 C 預(yù)處理器時,它會將 SWAP(a, b, int) 替換成 { int __tmp_c; __tmp_c = b; b = a; a = __tmp_c; } 。 文本替換是一種有效但是卻非常有限的特性。這種特性有以下問題: - 文本替換在與其他表達式一起使用時,可能會變得非常混亂。
- C 預(yù)處理器對于自己的宏只允許使用有限數(shù)目的參數(shù)。
- 由于 C 語言的類型系統(tǒng),通常需要對不同類型的參數(shù)定義不同的宏,至少必須傳遞一個參數(shù)類型作為參數(shù)。
- 由于只進行文本替換,因此如果這與傳遞給它的參數(shù)沖突,C 語言就無法智能地對臨時變量重新進行命名。如果傳遞
__tmp_c 變量,那么我們這個宏就會完全失敗了。
在表達式中合并宏的問題使得編寫宏非常困難。例如,假設(shè)已經(jīng)定義了下面這個 MIN 宏,它返回兩個值中的較小值: 清單 3. 返回兩個值中較小值的宏
#define MIN(x, y) ((x) > (y) ? (y) : (x))
|
首先,您可能會奇怪為什么此處使用了這么多的括號。原因是操作符的優(yōu)先順序。例如我們要執(zhí)行 MIN(27, b=32) ,如果沒有這些括號,這個表達式就會擴展成 27 > b = 32 ? b = 32 : 27 ,這會產(chǎn)生一個編譯器錯誤,因為按照操作符的優(yōu)先順序,27 > b 會連接在一起。如果在定義宏時使用了這些括號,那它就可以正常工作了。 不幸的是,這里還有一個問題。任何作為參數(shù)調(diào)用的函數(shù)每次都會被列到右邊。記住,預(yù)處理器并不了解 C 語言的任何內(nèi)容,它只是簡單地進行文本替換。因此,如果執(zhí)行一條語句 MIN(do_long_calc(), do_long_calc2()) ,它就會擴展成 ( (do_long_calc()) > (do_long_calc2()) ? (do_long_calc2()) : (do_long_calc())) 。這樣執(zhí)行的時間會更長,因為每個計算都至少要執(zhí)行兩次。 如果這些計算有某些副作用(例如打印、修改全局變量等),那情況就更加嚴重了,因為這些副作用都會被處理兩次。如果這些函數(shù)每次調(diào)用時所返回的結(jié)果都不相同,那么這種“多次調(diào)用”的問題甚至?xí)屵@個宏返回錯誤的結(jié)果。 更多有關(guān) C 預(yù)處理器宏編程的內(nèi)容可以在 CPP 手冊中看到(請參閱 參考資料 一節(jié)中的鏈接)。 M4 宏預(yù)處理器 M4 宏處理器是最高級的文本宏處理系統(tǒng)之一。它的聲望主要是由于這是流行的 sendmail 配置文件所使用的輔助工具。 sendmail 的配置既不有趣,也不簡單。sendmail 的配置文件就有一整本書專門來講解。然而,sendmail 的創(chuàng)造者編寫了一些 M4 宏來簡化這個處理過程。在這些宏中,您可以簡單地指定某些特定的參數(shù),M4 處理器可以對一個樣板文件進行操作,這個文件是特定于本地安裝和 sendmail 的通用設(shè)置的。這樣就可以為您提供一個配置文件了。 例如,清單 4 給出了一個典型的 sendmail 配置文件的 M4 宏: 清單 4. 使用 M4 宏的樣例 sendmail 配置文件
divert(-1)include(`/usr/share/sendmail-cf/m4/cf.m4‘)VERSIONID(`linux setup for my Linux dist‘)dnlOSTYPE(`linux‘)define(`confDEF_USER_ID‘,``8:12‘‘)dnlundefine(`UUCP_RELAY‘)dnlundefine(`BITNET_RELAY‘)dnldefine(`PROCMAIL_MAILER_PATH‘,`/usr/bin/procmail‘)dnldefine(`ALIAS_FILE‘, `/etc/aliases‘)dnldefine(`UUCP_MAILER_MAX‘, `2000000‘)dnldefine(`confUSERDB_SPEC‘, `/etc/mail/userdb.db‘)dnldefine(`confPRIVACY_FLAGS‘, `authwarnings,novrfy,noexpn,restrictqrun‘)dnldefine(`confAUTH_OPTIONS‘, `A‘)dnldefine(`confTO_IDENT‘, `0‘)dnlFEATURE(`no_default_msa‘,`dnl‘)dnlFEATURE(`smrsh‘,`/usr/sbin/smrsh‘)dnlFEATURE(`mailertable‘,`hash -o /etc/mail/mailertable.db‘)dnlFEATURE(`virtusertable‘,`hash -o /etc/mail/virtusertable.db‘)dnlFEATURE(redirect)dnlFEATURE(always_add_domain)dnlFEATURE(use_cw_file)dnlFEATURE(use_ct_file)dnlFEATURE(local_procmail,`‘,`procmail -t -Y -a $h -d $u‘)dnlFEATURE(`access_db‘,`hash -T<TMPF> -o /etc/mail/access.db‘)dnlFEATURE(`blacklist_recipients‘)dnlEXPOSED_USER(`root‘)dnlDAEMON_OPTIONS(`Port=smtp,Addr=127.0.0.1, Name=MTA‘)FEATURE(`accept_unresolvable_domains‘)dnlMAILER(smtp)dnlMAILER(procmail)dnlCwlocalhost.localdomain
|
您并不需要理解這些配置的具體含義,只需要知道存在這個文件就可以了。在 M4 宏處理這個文件之后,就會生成大約 1,000 行的配置。 類似地,autoconf 使用 M4 宏基于簡單的宏來生成 shell 腳本。如果您曾經(jīng)在安裝程序時首先輸入 ./configure ,那么就可能使用了一個由 autoconf 宏所生成的程序。清單 5 是一個簡單的 autoconf 程序,它生成了一個超過 3,000 行的 configure 程序: 清單 5. 使用 M4 宏的 autoconf 腳本
AC_INIT(hello.c)AM_CONFIG_HEADER(config.h)AM_INIT_AUTOMAKE(hello,0.1)AC_PROG_CCAC_PROG_INSTALLAC_OUTPUT(Makefile)
|
在宏處理器運行這個腳本時,會創(chuàng)建一個 shell 腳本,它會進行標(biāo)準的配置檢查,查找標(biāo)準的路徑和編譯器命令,并從模板中為您構(gòu)建 config.h 和 Makefile 文件。 這些 M4 宏處理器的詳細信息太過復(fù)雜,我們就不再在本文中進行介紹了,不過在 參考資料 一節(jié)中給出了有關(guān) M4 宏處理器及其在 sendmail 和 autoconf 中的用法的鏈接。
用來編寫程序的程序 現(xiàn)在讓我們把注意力從通用的文本替換程序轉(zhuǎn)移到專用的代碼生成器上來。我們將介紹幾個例子,了解一下樣例用法,并構(gòu)建一個代碼生成器。 代碼生成器的考慮因素 GNU/Linux 系統(tǒng)提供了幾個用來編寫程序的程序。最常見的有: - Flex,這是一個詞匯分析器生成器
- Bison,語法分析器生成器
- Gperf,一個很好的 hash 函數(shù)生成器
這些工具都可以為 C 語言生成一些文件。您可能會納悶為什么這些都是作為代碼生成器實現(xiàn)的,而不是作為函數(shù)實現(xiàn)的。原因有幾個方面: - 這些函數(shù)的輸入都非常復(fù)雜,不容易使用一種有效的 C 代碼格式來表示。
- 這些程序會為操作生成很多靜態(tài)的查找表,因此在預(yù)編譯時一次生成這些表比每次調(diào)用這個程序時都生成這些表更好。
- 這些系統(tǒng)的很多功能都是可以使用某些特定位置的任意代碼進行定制的。這些代碼然后就可以使用代碼生成器所生成的結(jié)構(gòu)中的變量和功能了,而不需要手工生成這些變量。
每個工具都著重于構(gòu)建一種特定類型的系統(tǒng)。Bison 用來生成語法分析器;Flex 用來生成詞匯分析器。其他工具用來實現(xiàn)編程中的自動化部分。 例如,將數(shù)據(jù)庫訪問方法集成到一種語言中通常非常繁瑣。要讓這個過程變得又簡單、又標(biāo)準化,那么嵌入式 SQL 就是一個很好的元編程系統(tǒng),可以在 C 語言中簡單地合并數(shù)據(jù)庫訪問的功能。 雖然在 C 語言中有很多庫可以用來訪問數(shù)據(jù)庫,但是使用諸如嵌入式 SQL 之類的代碼生成器可以使合并 C 和數(shù)據(jù)庫訪問的功能更加簡單:它將 SQL 實體的功能作為語言的一種擴展合并到了 C 語言中。然而,很多嵌入式 SQL 的實現(xiàn)通常都是一些專用的宏處理器,可以生成 C 程序作為輸出結(jié)果。使用嵌入式 SQL 可以讓對數(shù)據(jù)庫的訪問比直接使用庫函數(shù)來訪問數(shù)據(jù)庫更加自然、直觀,而且程序員可以更少犯錯誤。使用嵌入式 SQL,數(shù)據(jù)庫編程的復(fù)雜性可以通過一些宏子語言來屏蔽。 如何使用代碼生成器 為了了解代碼生成器是如何工作的,讓我們先來看一個簡短的嵌入式 SQL 程序。為了實現(xiàn)這種功能,需要使用一個嵌入式 SQL 的處理程序。PostgreSQL 就提供了一個嵌入式 SQL 的編譯器 ecpg 。要運行這個程序,需要在 PostgreSQL 中創(chuàng)建一個數(shù)據(jù)庫“test”。然后在這個數(shù)據(jù)庫中執(zhí)行下面的命令: 清單 6. 樣例程序的數(shù)據(jù)庫創(chuàng)建腳本
create table people (id serial primary key, name varchar(50));insert into people (name) values (‘Tony‘);insert into people (name) values (‘Bob‘);insert into people (name) values (‘Mary‘);
|
清單 7 是一個簡單的程序,它從數(shù)據(jù)庫中讀出數(shù)據(jù)的內(nèi)容,并將其打印到屏幕上,在打印時對 name 域進行排序: 清單 7. 嵌入式 SQL 程序的例子
#include <stdio.h>int main(){ /* Setup database connection -- replace postgres/password w/ the username/password on your system*/ EXEC SQL CONNECT TO unix:postgresql://localhost/test USER postgres/password; /* These variables are going to be used for temporary storage w/ the database */ EXEC SQL BEGIN DECLARE SECTION; int my_id; VARCHAR my_name[200]; EXEC SQL END DECLARE SECTION; /* This is the statement we are going to execute */ EXEC SQL DECLARE test_cursor CURSOR FOR SELECT id, name FROM people ORDER BY name; /* Run the statement */ EXEC SQL OPEN test_cursor; EXEC SQL WHENEVER NOT FOUND GOTO close_test_cursor; while(1) /* our previous statement will handle exitting the loop */ { /* Fetch the next value */ EXEC SQL FETCH test_cursor INTO :my_id, :my_name; printf("Fetched ID is %d and fetched name is %s\n", my_id, my_name.arr); } /* Cleanup */ close_test_cursor: EXEC SQL CLOSE test_cursor; EXEC SQL DISCONNECT; return 0;}
|
如果您以前曾經(jīng)在 C 語言中使用普通的數(shù)據(jù)庫庫函數(shù)編寫過訪問數(shù)據(jù)庫的程序,就會看出這是一種非常自然的編寫代碼的方法。正常的 C 編碼不允許返回多個任意類型的值,但是 EXEC SQL FETCH 卻可以返回多行結(jié)果。 要編譯并運行這個程序,只需要將其保存到 test.pgc 文件中,并運行下面的命令: 清單 8. 編譯嵌入式 SQL 程序
ecpg test.pgcgcc test.c -lecpg -o test./test
|
構(gòu)建代碼生成器 現(xiàn)在您已經(jīng)見過了幾種代碼生成器,了解了這些代碼生成器可以實現(xiàn)怎樣的功能,接下來我們應(yīng)該開始編寫一個小型的代碼生成器了??梢跃帉懙淖詈唵蔚目捎么a生成器也許就是構(gòu)建一個靜態(tài)查找表。通常,為了在 C 編程中構(gòu)建快速的函數(shù),只需要簡單地創(chuàng)建一個快速查找表,其中保存了所有的結(jié)果。這意味著可能需要手工提前計算好(這會浪費很多時間),也可以在運行時構(gòu)建(這會浪費用戶的時間)。 在這個例子中,我們將構(gòu)建一個代碼生成器,它要對一個整數(shù)執(zhí)行一個或一組函數(shù),并為結(jié)果構(gòu)建一個查找表。 要思考如何構(gòu)建這樣一個程序,讓我們從最后入手,并從后往前逐一解決問題。假設(shè)我們希望得到這樣一個查找表:它返回 5 到 20 之間各個數(shù)字的平方根。我們可以編寫一個簡單的程序來生成這樣一個查找表,例如: 清單 9. 生成并使用一個平方根查找表
/* our lookup table */double square_roots[21];/* function to load the table at runtime */void init_square_roots(){ int i; for(i = 5; i < 21; i++) { square_roots[i] = sqrt((double)i); }}/* program that uses the table */int main (){ init_square_roots(); printf("The square root of 5 is %f\n", square_roots[5]); return 0;}
|
現(xiàn)在,要將這些結(jié)果轉(zhuǎn)換成一個靜態(tài)初始化的數(shù)組,我們需要刪除這個程序的前半部分,并將其替換成手工計算出來的結(jié)果,如下所示: 清單 10. 帶靜態(tài)查找表的平方根程序
double square_roots[] = { /* these are the ones we skipped */ 0.0, 0.0, 0.0, 0.0, 0.0 2.236068, /* Square root of 5 */ 2.449490, /* Square root of 6 */ 2.645751, /* Square root of 7 */ 2.828427, /* Square root of 8 */ 3.0, /* Square root of 9 */ ... 4.472136 /* Square root of 20 */};
|
我們需要的是這樣一個程序,它可以生成這些結(jié)果,并將其輸出到上面這樣的表中,這樣就可以在編譯時加載了。 下面讓我們分析一下要解決哪些問題: - 數(shù)組名
- 數(shù)組類型
- 起始索引
- 結(jié)束索引
- 忽略項的缺省值
- 計算最終值的表達式
這些都非常簡單,并且進行了很好的定義 —— 它們可以作為一個簡單的列表進行輸出。因此我們可能會希望執(zhí)行宏調(diào)用,將這些元素合并到一個使用冒號進行分隔的列表中,如下所示: 清單 11. 生成編譯時平方根表的理想方法
/* sqrt.in *//* Our macro invocation to build us the table. The format is: *//* TABLE:array name:type:start index:end index:default:expression *//* VAL is used as the placeholder for the current index in the expression */TABLE:square_roots:double:5:20:0.0:sqrt(VAL)int main(){ printf("The square root of 5 is %f\n", square_roots[5]); return 0;}
|
現(xiàn)在我們只需要一個程序?qū)⑦@個宏轉(zhuǎn)換成標(biāo)準的 C 語言就可以了。對于這個簡單的例子來說,我們將使用 Perl 來實現(xiàn)這個程序,因為它可以對字符串中的用戶代碼進行評測,其語法也與 C 語言非常類似。這樣我們就可以動態(tài)加載并處理用戶代碼了。 代碼生成器應(yīng)該處理宏的聲明,但是對于所有非宏的部分都應(yīng)該不加任何修改地傳遞。因此,宏處理器的基本組織應(yīng)該是: - 讀入一行。
- 判斷該行是否應(yīng)該進行處理?
- 如果應(yīng)該,就對該行進行處理,并生成輸出結(jié)果。
- 否則,就簡單地將這一行的內(nèi)容不加任何修改,直接拷貝到輸出中。
清單 12 是創(chuàng)建這個表生成器所使用的 Perl 代碼: 清單 12. 這個表宏的代碼生成器
#!/usr/bin/perl##tablegen.pl###Puts each program line into $linewhile(my $line = <>){ #Is this a macro invocation? if($line =~ m/TABLE:/) { #If so, split it apart into its component pieces my ($dummy, $table_name, $type, $start_idx, $end_idx, $default, $procedure) = split(m/:/, $line, 7); #The main difference between C and Perl for mathematical expressions is that #Perl prefixes its variables with a dollar sign, so we will add that here $procedure =~ s/VAL/\$VAL/g; #Print out the array declaration print "${type} ${table_name} [] = {\n"; #Go through each array element foreach my $VAL (0 .. $end_idx) { #Only process an answer if we have reached our starting index if($VAL >= $start_idx) { #evaluate the procedure specified (this sets $@ if there are any errors) $result = eval $procedure; die("Error processing: $@") if $@; } else { #if we haven‘t reached the starting index, just use the default $result = $default; } #Print out the value print "\t${result}"; #If there are more to be processed, add a comma after the value if($VAL != $end_idx) { print ","; } print "\n" } #Finish the declaration print "};\n"; } else { #If this is not a macro invocation, just copy the line directly to the output print $line; }}
|
要運行這個程序,請執(zhí)行下面的命令: 清單 13. 運行代碼生成器
./tablegen.pl < sqrt.in > sqrt.cgcc sqrt.c -o sqrt./a.out
|
這樣只需要剛才創(chuàng)建的這個簡單代碼生成器中的幾行代碼,我們就可以極大地簡化編程任務(wù)。使用這一個宏,就可以節(jié)省很多編程的工作,它可以生成一個使用整數(shù)進行索引的數(shù)學(xué)表。我們還要實現(xiàn)另外一些任務(wù):讓這個表包含完整的結(jié)構(gòu)定義;還要確保這個數(shù)組前面沒有空項,這樣就不會浪費空間。
使用 Scheme 編寫對語言敏感的宏 盡管代碼生成器可以理解一點兒目標(biāo)語言的知識,但是它們通常都不是完整的語法分析器,不重新編寫一個完整的編譯器是無法全面考慮目標(biāo)語言的。 然而,如果有一種語言已經(jīng)使用一個簡單的數(shù)據(jù)結(jié)構(gòu)進行了表示,那么這種情況就可以簡化了。在 Scheme 編程語言中,這種語言本身可以表示成一個鏈表,并且 Scheme 編程語言就是為進行列表處理而開發(fā)的。這使得 Scheme 非常適合于創(chuàng)建被轉(zhuǎn)換的程序,要對程序進行分析并不需要大量的處理,Scheme 本身就是一種列表處理語言。 實際上,Scheme 用來實現(xiàn)轉(zhuǎn)換的功能已經(jīng)超出了本文的范圍。Scheme 標(biāo)準定義了一種專門用來簡化對其他語言進行擴展的宏語言。大部分 Scheme 的實現(xiàn)都提供了一些特性來輔助構(gòu)建代碼生成程序。 讓我們重新研究一下 C 宏中的問題。使用 SWAP 宏,首先必須要顯式地說明要交換的值的類型,必須要為臨時變量使用一個名字,并且要確保這個名字沒有在其他地方使用。讓我們來看一下 Scheme 的等效代碼,以及 Scheme 是如何解決這個問題的: 清單 14. Scheme 中的值交換
;;Define SWAP to be a macro(define-syntax SWAP ;;We are using the syntax-rules method of macro-building (syntax-rules () ;;Rule Group ( ;;This is the pattern we are matching (SWAP a b) ;;This is what we want it to transform into (let ( (c b)) (set! b a) (set! a c)))))(define first 2)(define second 9)(SWAP first second)(display "first is: ")(display first)(newline)(display "second is: ")(display second)(newline)
|
這是一個 syntax-rules 宏。Scheme 有幾個宏系統(tǒng),但是 syntax-rules 是其中最標(biāo)準的。 在 syntax-rules 宏中,define-syntax 是用來定義宏轉(zhuǎn)換的關(guān)鍵字。在 define-syntax 關(guān)鍵字之后是要定義的宏的名字;之后是要轉(zhuǎn)換的內(nèi)容。 syntax-rules 是要采用的轉(zhuǎn)換類型。在圓括號中的是正在使用的其他符號,而不是宏名本身(在這個例子中沒有宏名)。
之后是一系列轉(zhuǎn)換規(guī)則。這種語法轉(zhuǎn)換器會遍歷每條規(guī)則,并試圖查找一個匹配的模式。在找到這樣一個模式之后,就執(zhí)行指定的轉(zhuǎn)換操作。在這個例子中,只有一個模式:(SWAP a b) 。a 和 b 是 模式變量(pattern variable),它們與宏調(diào)用中的代碼單元進行匹配,并且用來重新安排轉(zhuǎn)換過程中的部分。 表面上來看,這與 C 版本的程序具有同樣的缺陷;然而實際上它們之間存在很多不同之處。首先,由于這個宏采用的是 Scheme 語言,因此類型都已經(jīng)被綁定到值本身上面了,而不是綁定到變量名上面,因此根本不用擔(dān)心會出現(xiàn) C 版本中那種變量類型的問題。但是它是否也有原來的變量名問題呢?如果一個變量被命名為 c ,那么這不會產(chǎn)生沖突嗎? 實際上的確不會。Scheme 中使用 syntax-rules 的宏都是 hygienic。這意味著宏所使用的所有臨時變量都會在 替換發(fā)生之前 自動重新進行命名,從而防止名字產(chǎn)生沖突。因此在這個宏中,如果替換變量是 c ,那么在替換發(fā)生之前 c 就會被重新命名成其他的名字。實際上,此時通常都會重新進行命名。清單 15 是對這個程序進行宏轉(zhuǎn)換的一種可能的結(jié)果: 清單 15. 值交換宏的一種可能轉(zhuǎn)換結(jié)果
(define first 2)(define second 9)(let ( (__generated_symbol_1 second)) (set! second first) (set! first __generated_symbol_1))(display "first is: ")(display first)(newline)(display "second is: ")(display second)(newline)
|
正如您可以看到的一樣,Scheme 的宏可以提供其他宏系統(tǒng)的優(yōu)點,卻沒有那些系統(tǒng)的問題。 然而,有時您可能會希望宏不是 hygienic 的。例如,可能希望在那些正在轉(zhuǎn)換的代碼中綁定這個宏。簡單地聲明一個變量并不能實現(xiàn)這種功能,因為 syntax-rules 系統(tǒng)會對變量重新進行命名。因此,大部分模式還包含一個非 hygienic 的宏系統(tǒng),通常稱為 syntax-case 。 syntax-case 宏很難編寫,但是其功能更加強大,因為這樣就可以使用完整的 Scheme 系統(tǒng)功能來進行轉(zhuǎn)換了。syntax-case 宏并不是實際的標(biāo)準,但是它們在很多 Scheme 系統(tǒng)中都已經(jīng)實現(xiàn)了。沒有 syntax-case 宏的系統(tǒng)通常也會有其他類似的系統(tǒng)可以使用。
讓我們來看一下 syntax-case 宏的基本格式。讓我們來定義一個宏 at-compile-time ,它將在編譯時執(zhí)行一個給定的表單。 清單 16. 在編譯時生成單個值或成組值的宏
;;Define our macro(define-syntax at-compile-time ;;x is the syntax object to be transformed (lambda (x) (syntax-case x () ( ;;Pattern just like a syntax-rules pattern (at-compile-time expression) ;;with-syntax allows us to build syntax objects ;;dynamically (with-syntax ( ;this is the syntax object we are building (expression-value ;after computing expression, transform it into a syntax object (datum->syntax-object ;syntax domain (syntax k) ;quote the value so that its a literal value (list ‘quote ;compute the value to transform (eval ;;convert the expression from the syntax representation ;;to a list representation (syntax-object->datum (syntax expression)) ;;environment to evaluate in (interaction-environment) ))))) ;;Just return the generated value as the result (syntax expression-value))))))(define a ;;converts to 5 at compile-time (at-compile-time (+ 2 3)))
|
它可以在編譯時執(zhí)行給定的操作。更具體地說,它是在宏展開時執(zhí)行給定的操作,在 Scheme 系統(tǒng)中宏展開與編譯并不總是同時進行的。Scheme 系統(tǒng)中編譯時允許執(zhí)行的任何表達式都可以在這個表達式中使用?,F(xiàn)在讓我們來看一下這是如何工作的。 使用 syntax-case 系統(tǒng),實際上是在定義一個轉(zhuǎn)換函數(shù),這就是 lambda 發(fā)揮作用的地方。x 是正在轉(zhuǎn)換的表達式。with-syntax 額外定義了一些語法元素,可以在轉(zhuǎn)換表達式中使用。syntax 可以使用這些語法元素,并將其組合在一起,它遵循與 syntax-rules 中相同的轉(zhuǎn)換規(guī)則。讓我們來看一下每個步驟中會發(fā)生什么操作: at-compile-time 表達式匹配。 - 在轉(zhuǎn)換最內(nèi)部的地方,
expression 被轉(zhuǎn)換成一個列表,并作為普通的模式代碼進行分析。 - 然后,結(jié)果與符號
quote 合并到一個列表中,這樣 Scheme 就會在將其轉(zhuǎn)換成代碼時將其作為一個文本值進行處理。 - 這些數(shù)據(jù)被轉(zhuǎn)換成一個 syntax 對象。
- 這個 syntax 對象在輸出結(jié)果中使用名字
expression-value 表示。 - 轉(zhuǎn)換器
(syntax expression-value) 認為 expression-value 是這個宏的全部輸出。
利用這種在編譯時執(zhí)行計算的功能,我們可以編寫一個比 C 語言更好的 TABLE 宏。清單 17 顯示了在 Scheme 中應(yīng)該如何使用 at-compile-time 宏: 清單 17. 在 Scheme 中構(gòu)建平方根表
(define sqrt-table (at-compile-time (list->vector (let build ( (val 0)) (if (> val 20) ‘() (cons (sqrt val) (build (+ val 1))))))))(display (vector-ref sqrt-table 5))(newline)
|
可以通過對這個宏進一步進行處理生成一個用來構(gòu)建表的宏,進一步進行簡化,這與前面的 C 語言版本的宏類似: 清單 18. 用來在編譯時構(gòu)建查找表的宏
(define-syntax build-compiled-table (syntax-rules () ( (build-compiled-table name start end default func) (define name (at-compile-time (list->vector (let build ( (val 0)) (if (> val end) ‘() (if (< val start) (cons default (build (+ val 1))) (cons (func val) (build (+ val 1))))))))))))(build-compiled-table sqrt-table 5 20 0.0 sqrt)(display (vector-ref sqrt-table 5))(newline)
|
現(xiàn)在,有了一個可以簡單地構(gòu)建任何想要的表的函數(shù)。
結(jié)束語 我們已經(jīng)介紹了很多知識,因此現(xiàn)在花一分鐘來回顧一下。首先我們討論了哪些問題最適合使用代碼生成程序來解決。這包括以下問題: - 需要提前生成數(shù)據(jù)表的程序
- 有大量樣板文件的程序,但是無法抽象成函數(shù)
- 使用開發(fā)語言不具備的特性的程序
然后我們介紹了幾種元編程系統(tǒng),并給出了幾個使用這些系統(tǒng)的例子。這包括通用文本替換系統(tǒng),以及領(lǐng)域特有的程序和函數(shù)生成器。然后又介紹了一個具體的構(gòu)建表的示例,并介紹了用 C 編寫這樣一個代碼生成程序來構(gòu)建靜態(tài)表的詳細過程。 最后,我們介紹了 Scheme,并了解了它如何解決我們在 C 語言中所面對的問題:它使用了一些結(jié)構(gòu),而這些結(jié)構(gòu)本身就是 Scheme 語言的一部分。Scheme 既是一種語言,又是一種代碼生成語言。由于這些技術(shù)都已經(jīng)構(gòu)建到了語言本身中,因此很容易編寫程序,并且不會碰到其他語言中所面臨的問題。這樣我們就可以為 Scheme 語言在代碼生成器傳統(tǒng)應(yīng)用的地方簡單地添加一些領(lǐng)域特有的擴展了。 本系列文章的第 2 部分將詳細介紹如何編寫 Scheme 宏,以及如何使用這些宏來極大地簡化大型編程任務(wù)。
參考資料 學(xué)習(xí)
獲得產(chǎn)品和技術(shù)
- 索取免費的 SEK for Linux,這有兩張 DVD,包括最新的 IBM for Linux 的試用版軟件,包括 DB2?、Lotus?、Rational?、Tivoli? 和 WebSphere?。
- 在您的下一個 Linux 開發(fā)項目中采用 IBM 試用版軟件,這可以從 developerWorks 上直接下載。
討論
|