免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
元編程藝術(shù):程序的程序

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.hMakefile 文件。

這些 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)該是:

  1. 讀入一行。
  2. 判斷該行是否應(yīng)該進行處理?
  3. 如果應(yīng)該,就對該行進行處理,并生成輸出結(jié)果。
  4. 否則,就簡單地將這一行的內(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)。ab模式變量(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ā)生什么操作:

  1. at-compile-time 表達式匹配。
  2. 在轉(zhuǎn)換最內(nèi)部的地方,expression 被轉(zhuǎn)換成一個列表,并作為普通的模式代碼進行分析。
  3. 然后,結(jié)果與符號 quote 合并到一個列表中,這樣 Scheme 就會在將其轉(zhuǎn)換成代碼時將其作為一個文本值進行處理。
  4. 這些數(shù)據(jù)被轉(zhuǎn)換成一個 syntax 對象。
  5. 這個 syntax 對象在輸出結(jié)果中使用名字 expression-value 表示。
  6. 轉(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í)
  • 您可以參閱本文在 developerWorks 全球站點上的 英文原文 。

  • 在 “通過有效處理列表更好地實現(xiàn)編程”(developerWorks,2005 年 1 月)中,Jonathan 介紹了單鏈表和 Scheme 技術(shù)。

  • Using XML and XSL for code generation”(developerWorks,2001 年 5 月)介紹了 XSL 樣式表的一種不太常見的用法:通過將 XML 文檔轉(zhuǎn)換成源代碼來為常見的設(shè)備創(chuàng)建應(yīng)用程序。

  • Code generation using XSLT 教程(developerWorks,2003 年 4 月)對代碼生成的概念進行了基本的介紹。

  • 用代碼生成取代反射”(developerWorks,2004 年 6 月)展示了如何使用運行時類處理來用生成的代碼替換反射代碼。

  • GNU CPP manual 中有很多有關(guān) GCC 宏處理功能的信息,以及在編程時您需要了解的一些警告。

  • 有關(guān) M4 的更多信息,請參閱 manual for GNU M4。

  • 如果您希望了解更多有關(guān) Sendmail 的 M4 宏的知識,請參閱 official Sendmail M4 macro reference

  • 要了解更多有關(guān) GNU 編譯系統(tǒng)以及如何使用 autoconf 和 M4 宏來實現(xiàn)自動化操作的更多知識,請參閱 GNU build system tutorial。

  • 這些 教程 可以讓您入手學(xué)習(xí)使用 syntax-casesyntax-rules 的 Scheme 宏編程。

  • Code Generation in Action (Manning,2003)介紹了構(gòu)建高質(zhì)量的機器生成的代碼所采用的技術(shù)和實現(xiàn),并給出了構(gòu)建各種代碼生成器的詳細步驟。

  • developerWorks Linux 專區(qū) 中可以找到為 Linux 開發(fā)人員準備的更多參考資料。

  • 緊密跟蹤 developerWorks 技術(shù)動態(tài)和事件。

獲得產(chǎn)品和技術(shù)
  • 索取免費的 SEK for Linux,這有兩張 DVD,包括最新的 IBM for Linux 的試用版軟件,包括 DB2?、Lotus?、Rational?、Tivoli? 和 WebSphere?。

  • 在您的下一個 Linux 開發(fā)項目中采用 IBM 試用版軟件,這可以從 developerWorks 上直接下載。



討論
  • 有關(guān)編程的一個非常有用的資源是 Code Generation Network,它為注重實效的工程師提供了代碼生成方面的信息。

  • 通過參與 developerWorks blogs 來加入 developerWorks 社區(qū)。
本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
高階函數(shù)
閉包的概念、形式與應(yīng)用
與Scheme共舞
王垠:如何掌握程序語言
SQLite編譯器部分淺析
C 和 C++ 的矩陣庫
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服