Loading

元编程艺术,第 1 部分: 元编程简介

[原文链接]

  编写程序来生成其他程序 

  目前应用最广泛的技术之一是编写生成其他程序或部分程序的程序。因此十分有必要学习为什么要采用元编程,以及元编程都有哪些组件(文本宏语言,专用代码生成器)。在本文中,您将学习到如何构建一个代码生成器,并详细了解如何使用 Scheme 编写对语言敏感的宏。

用来生成代码的程序有时被称为 元程序(metaprogram);编写这种程序就称为 元编程(metaprogramming)。编写这种输出代码的程序可以有无数的应用。

本文将介绍为什么会考虑进行元编程,并介绍这种技术的一些组件 —— 我们将深入介绍文本宏语言(textual macro language),了解专用的代码生成器,并讨论如何构建这些工具,最后研究如何使用 Scheme 编写对语言敏感的宏。

元编程的不同用法

  首先,可以编写一些程序来提前生成一些数据供运行时使用。例如,如果您正在开发一个游戏,并且希望使用一个所有 8 位整数的正弦值的查询表,既可以每次都执行正弦计算的操作,也可以让程序在启动时构建这样的一张表在运行时使用,或者编写一个程序在编译之前为这个表生成定制代码。尽管对于少量的数据来说在运行时构建这张表是可能的,但是有些任务则可能会使得程序启动非常缓慢。在这种情况中,编写一个程序来构建一张静态表通常是最好的解决方案。

其次,如果您有一个很大的应用程序,这个程序有很多函数都包括了很多样板文件,那么就可以创建一个小型的语言,它可以生成这些样板代码,让您可以只实现重要的部分。现在,如果可以,最好是能够将这些样板部分抽象成一个函数。但是通常来说,这些样板代码并不会如此精美。可能每个实例中都需要声明一些变量,可能需要注册错误处理程序,或者有一些样板文件必须在某些情况中插入一些代码。所有这些都使得简单的函数调用是不可能的。在这种情况中,通常创建一个小型的语言来更简单地利用样板文件的代码。这种小型的语言可以在编译之前被转换成普通的源代码语言。

最后,有很多编程语言都可以编写非常复杂的语句来真正实现一些功能。代码生成程序可以对这种语句进行简化,并节省很多输入的工作,这可以防止大量的输入错误,因为减少了很多输入错误内容的机会。

作为语言可以有很多特性,代码生成程序就不需要这么多了。一种语言中的标准特性在另外一种语言中可能只能通过代码生成程序实现。然而,语言设计不充分并不是需要代码生成程序的唯一原因。维护简单也是一个原因。

 

文本宏语言基础

代码生成程序允许您开发并使用小型的、领域特有的语言,这样比直接在目标语言中开发这种功能更容易编写和维护。

用来创建领域特有语言的工具通常称为 宏语言(macro language)。本文介绍了几种宏语言的方法,并介绍了如何改进代码。

C 预处理器(CPP)

首先让我们来看一下涉及文本宏编程的元编程。文本宏(textual macro) 是可以直接影响编程语言中的文本的宏,它们并不需要了解或处理语言的意义。两个最广泛使用的文本宏系统是 C 预处理器和 M4 宏处理器。

如果您曾经使用 C 进行过编程,那么可能处理过 C 语言中的 #define 宏。文本宏的扩展虽然不甚理想,但在很多没有更好的代码生成能力的语言中,这是用来进行基本元编程的一种简单方法。清单 1 给出了一个 #define 宏的例子:

清单 1. 用来交换两个值的宏
#define SWAP(a, b, type) { type __tmp_c; c = b; b = a; a = c; }

这个宏可以交换两个给定类型的值。由于几个原因,这最好是作为一个宏来实现:

  • 对于这种简单的操作来说,函数调用的开销太大。
  • 需要向函数传递变量的地址而不是变量的值。(这并不是很大的问题,但是传递值会使函数调用比较混乱,并且编辑器就无法将这些值保存在寄存器中了。)
  • 对于每种需要交换的类型来说,都需要定义一个不同的函数。

清单 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;
}

当运行 C 预处理器时,它会将 SWAP(a, b, int) 替换成 { int __tmp_c; __tmp_c = b; b = a; a = __tmp_c; }

文本替换是一种有效但是却非常有限的特性。这种特性有以下问题:

  • 文本替换在与其他表达式一起使用时,可能会变得非常混乱。
  • C 预处理器对于自己的宏只允许使用有限数目的参数。
  • 由于 C 语言的类型系统,通常需要对不同类型的参数定义不同的宏,至少必须传递一个参数类型作为参数。
  • 由于只进行文本替换,因此如果这与传递给它的参数冲突,C 语言就无法智能地对临时变量重新进行命名。如果传递 __tmp_c 变量,那么我们这个宏就会完全失败了。

在表达式中合并宏的问题使得编写宏非常困难。例如,假设已经定义了下面这个 MIN 宏,它返回两个值中的较小值:

清单 3. 返回两个值中较小值的宏
#define MIN(x, y) ((x) > (y) ? (y) : (x))

首先,您可能会奇怪为什么此处使用了这么多的括号。原因是操作符的优先顺序。例如我们要执行 MIN(27, b=32),如果没有这些括号,这个表达式就会扩展成 27 > b = 32 ? b = 32 : 27,这会产生一个编译器错误,因为按照操作符的优先顺序,27 > b 会连接在一起。如果在定义宏时使用了这些括号,那它就可以正常工作了。

不幸的是,这里还有一个问题。任何作为参数调用的函数每次都会被列到右边。记住,预处理器并不了解 C 语言的任何内容,它只是简单地进行文本替换。因此,如果执行一条语句 MIN(do_long_calc(), do_long_calc2()),它就会扩展成 ( (do_long_calc()) > (do_long_calc2()) ? (do_long_calc2()) : (do_long_calc()))。这样执行的时间会更长,因为每个计算都至少要执行两次。

如果这些计算有某些副作用(例如打印、修改全局变量等),那情况就更加严重了,因为这些副作用都会被处理两次。如果这些函数每次调用时所返回的结果都不相同,那么这种“多次调用”的问题甚至会让这个宏返回错误的结果。

更多有关 C 预处理器宏编程的内容可以在 CPP 手册中看到(请参阅 参考资料 一节中的链接)。

M4 宏预处理器

M4 宏处理器是最高级的文本宏处理系统之一。它的声望主要是由于这是流行的 sendmail 配置文件所使用的辅助工具。

sendmail 的配置既不有趣,也不简单。sendmail 的配置文件就有一整本书专门来讲解。然而,sendmail 的创造者编写了一些 M4 宏来简化这个处理过程。在这些宏中,您可以简单地指定某些特定的参数,M4 处理器可以对一个样板文件进行操作,这个文件是特定于本地安装和 sendmail 的通用设置的。这样就可以为您提供一个配置文件了。

例如,清单 4 给出了一个典型的 sendmail 配置文件的 M4 宏:

清单 4. 使用 M4 宏的样例 sendmail 配置文件
divert(-1)
include(`/usr/share/sendmail-cf/m4/cf.m4')
VERSIONID(`linux setup for my Linux dist')dnl
OSTYPE(`linux')
define(`confDEF_USER_ID',``8:12'')dnl
undefine(`UUCP_RELAY')dnl
undefine(`BITNET_RELAY')dnl
define(`PROCMAIL_MAILER_PATH',`/usr/bin/procmail')dnl
define(`ALIAS_FILE', `/etc/aliases')dnl
define(`UUCP_MAILER_MAX', `2000000')dnl
define(`confUSERDB_SPEC', `/etc/mail/userdb.db')dnl
define(`confPRIVACY_FLAGS', `authwarnings,novrfy,noexpn,restrictqrun')dnl
define(`confAUTH_OPTIONS', `A')dnl
define(`confTO_IDENT', `0')dnl
FEATURE(`no_default_msa',`dnl')dnl
FEATURE(`smrsh',`/usr/sbin/smrsh')dnl
FEATURE(`mailertable',`hash -o /etc/mail/mailertable.db')dnl
FEATURE(`virtusertable',`hash -o /etc/mail/virtusertable.db')dnl
FEATURE(redirect)dnl
FEATURE(always_add_domain)dnl
FEATURE(use_cw_file)dnl
FEATURE(use_ct_file)dnl
FEATURE(local_procmail,`',`procmail -t -Y -a $h -d $u')dnl
FEATURE(`access_db',`hash -T<TMPF> -o /etc/mail/access.db')dnl
FEATURE(`blacklist_recipients')dnl
EXPOSED_USER(`root')dnl
DAEMON_OPTIONS(`Port=smtp,Addr=127.0.0.1, Name=MTA')
FEATURE(`accept_unresolvable_domains')dnl
MAILER(smtp)dnl
MAILER(procmail)dnl
Cwlocalhost.localdomain

您并不需要理解这些配置的具体含义,只需要知道存在这个文件就可以了。在 M4 宏处理这个文件之后,就会生成大约 1,000 行的配置。

类似地,autoconf 使用 M4 宏基于简单的宏来生成 shell 脚本。如果您曾经在安装程序时首先输入 ./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_CC
AC_PROG_INSTALL
AC_OUTPUT(Makefile)

在宏处理器运行这个脚本时,会创建一个 shell 脚本,它会进行标准的配置检查,查找标准的路径和编译器命令,并从模板中为您构建config.h 和 Makefile 文件。

这些 M4 宏处理器的详细信息太过复杂,我们就不再在本文中进行介绍了,不过在 参考资料 一节中给出了有关 M4 宏处理器及其在 sendmail 和 autoconf 中的用法的链接。

 

用来编写程序的程序

现在让我们把注意力从通用的文本替换程序转移到专用的代码生成器上来。我们将介绍几个例子,了解一下样例用法,并构建一个代码生成器。

代码生成器的考虑因素

GNU/Linux 系统提供了几个用来编写程序的程序。最常见的有:

  • Flex,这是一个词汇分析器生成器
  • Bison,语法分析器生成器
  • Gperf,一个很好的 hash 函数生成器

这些工具都可以为 C 语言生成一些文件。您可能会纳闷为什么这些都是作为代码生成器实现的,而不是作为函数实现的。原因有几个方面:

  • 这些函数的输入都非常复杂,不容易使用一种有效的 C 代码格式来表示。
  • 这些程序会为操作生成很多静态的查找表,因此在预编译时一次生成这些表比每次调用这个程序时都生成这些表更好。
  • 这些系统的很多功能都是可以使用某些特定位置的任意代码进行定制的。这些代码然后就可以使用代码生成器所生成的结构中的变量和功能了,而不需要手工生成这些变量。

每个工具都着重于构建一种特定类型的系统。Bison 用来生成语法分析器;Flex 用来生成词汇分析器。其他工具用来实现编程中的自动化部分。

例如,将数据库访问方法集成到一种语言中通常非常繁琐。要让这个过程变得又简单、又标准化,那么嵌入式 SQL 就是一个很好的元编程系统,可以在 C 语言中简单地合并数据库访问的功能。

虽然在 C 语言中有很多库可以用来访问数据库,但是使用诸如嵌入式 SQL 之类的代码生成器可以使合并 C 和数据库访问的功能更加简单:它将 SQL 实体的功能作为语言的一种扩展合并到了 C 语言中。然而,很多嵌入式 SQL 的实现通常都是一些专用的宏处理器,可以生成 C 程序作为输出结果。使用嵌入式 SQL 可以让对数据库的访问比直接使用库函数来访问数据库更加自然、直观,而且程序员可以更少犯错误。使用嵌入式 SQL,数据库编程的复杂性可以通过一些宏子语言来屏蔽。

如何使用代码生成器

为了了解代码生成器是如何工作的,让我们先来看一个简短的嵌入式 SQL 程序。为了实现这种功能,需要使用一个嵌入式 SQL 的处理程序。PostgreSQL 就提供了一个嵌入式 SQL 的编译器 ecpg。要运行这个程序,需要在 PostgreSQL 中创建一个数据库“test”。然后在这个数据库中执行下面的命令:

清单 6. 样例程序的数据库创建脚本
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 是一个简单的程序,它从数据库中读出数据的内容,并将其打印到屏幕上,在打印时对 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;
}

如果您以前曾经在 C 语言中使用普通的数据库库函数编写过访问数据库的程序,就会看出这是一种非常自然的编写代码的方法。正常的 C 编码不允许返回多个任意类型的值,但是 EXEC SQL FETCH 却可以返回多行结果。

要编译并运行这个程序,只需要将其保存到 test.pgc 文件中,并运行下面的命令:

清单 8. 编译嵌入式 SQL 程序
ecpg test.pgc
gcc test.c -lecpg -o test
./test

构建代码生成器

现在您已经见过了几种代码生成器,了解了这些代码生成器可以实现怎样的功能,接下来我们应该开始编写一个小型的代码生成器了。可以编写的最简单的可用代码生成器也许就是构建一个静态查找表。通常,为了在 C 编程中构建快速的函数,只需要简单地创建一个快速查找表,其中保存了所有的结果。这意味着可能需要手工提前计算好(这会浪费很多时间),也可以在运行时构建(这会浪费用户的时间)。

在这个例子中,我们将构建一个代码生成器,它要对一个整数执行一个或一组函数,并为结果构建一个查找表。

要思考如何构建这样一个程序,让我们从最后入手,并从后往前逐一解决问题。假设我们希望得到这样一个查找表:它返回 5 到 20 之间各个数字的平方根。我们可以编写一个简单的程序来生成这样一个查找表,例如:

清单 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;
}

现在,要将这些结果转换成一个静态初始化的数组,我们需要删除这个程序的前半部分,并将其替换成手工计算出来的结果,如下所示:

清单 10. 带静态查找表的平方根程序
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 */
};

我们需要的是这样一个程序,它可以生成这些结果,并将其输出到上面这样的表中,这样就可以在编译时加载了。

下面让我们分析一下要解决哪些问题:

  • 数组名
  • 数组类型
  • 起始索引
  • 结束索引
  • 忽略项的缺省值
  • 计算最终值的表达式

这些都非常简单,并且进行了很好的定义 —— 它们可以作为一个简单的列表进行输出。因此我们可能会希望执行宏调用,将这些元素合并到一个使用冒号进行分隔的列表中,如下所示:

清单 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;
}

现在我们只需要一个程序将这个宏转换成标准的 C 语言就可以了。对于这个简单的例子来说,我们将使用 Perl 来实现这个程序,因为它可以对字符串中的用户代码进行评测,其语法也与 C 语言非常类似。这样我们就可以动态加载并处理用户代码了。

代码生成器应该处理宏的声明,但是对于所有非宏的部分都应该不加任何修改地传递。因此,宏处理器的基本组织应该是:

  1. 读入一行。
  2. 判断该行是否应该进行处理?
  3. 如果应该,就对该行进行处理,并生成输出结果。
  4. 否则,就简单地将这一行的内容不加任何修改,直接拷贝到输出中。

清单 12 是创建这个表生成器所使用的 Perl 代码:

清单 12. 这个表宏的代码生成器
#!/usr/bin/perl
#
#tablegen.pl
#
##Puts each program line into $line
while(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;
   }
}

要运行这个程序,请执行下面的命令:

清单 13. 运行代码生成器
./tablegen.pl < sqrt.in > sqrt.c
gcc sqrt.c -o sqrt
./a.out

这样只需要刚才创建的这个简单代码生成器中的几行代码,我们就可以极大地简化编程任务。使用这一个宏,就可以节省很多编程的工作,它可以生成一个使用整数进行索引的数学表。我们还要实现另外一些任务:让这个表包含完整的结构定义;还要确保这个数组前面没有空项,这样就不会浪费空间。

 

使用 Scheme 编写对语言敏感的宏

尽管代码生成器可以理解一点儿目标语言的知识,但是它们通常都不是完整的语法分析器,不重新编写一个完整的编译器是无法全面考虑目标语言的。

然而,如果有一种语言已经使用一个简单的数据结构进行了表示,那么这种情况就可以简化了。在 Scheme 编程语言中,这种语言本身可以表示成一个链表,并且 Scheme 编程语言就是为进行列表处理而开发的。这使得 Scheme 非常适合于创建被转换的程序,要对程序进行分析并不需要大量的处理,Scheme 本身就是一种列表处理语言。

实际上,Scheme 用来实现转换的功能已经超出了本文的范围。Scheme 标准定义了一种专门用来简化对其他语言进行扩展的宏语言。大部分 Scheme 的实现都提供了一些特性来辅助构建代码生成程序。

让我们重新研究一下 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 有几个宏系统,但是 syntax-rules 是其中最标准的。

在 syntax-rules 宏中,define-syntax 是用来定义宏转换的关键字。在 define-syntax 关键字之后是要定义的宏的名字;之后是要转换的内容。

syntax-rules 是要采用的转换类型。在圆括号中的是正在使用的其他符号,而不是宏名本身(在这个例子中没有宏名)。

之后是一系列转换规则。这种语法转换器会遍历每条规则,并试图查找一个匹配的模式。在找到这样一个模式之后,就执行指定的转换操作。在这个例子中,只有一个模式:(SWAP a b)a 和 b 是 模式变量(pattern variable),它们与宏调用中的代码单元进行匹配,并且用来重新安排转换过程中的部分。

表面上来看,这与 C 版本的程序具有同样的缺陷;然而实际上它们之间存在很多不同之处。首先,由于这个宏采用的是 Scheme 语言,因此类型都已经被绑定到值本身上面了,而不是绑定到变量名上面,因此根本不用担心会出现 C 版本中那种变量类型的问题。但是它是否也有原来的变量名问题呢?如果一个变量被命名为 c,那么这不会产生冲突吗?

实际上的确不会。Scheme 中使用 syntax-rules 的宏都是 hygienic。这意味着宏所使用的所有临时变量都会在 替换发生之前 自动重新进行命名,从而防止名字产生冲突。因此在这个宏中,如果替换变量是 c,那么在替换发生之前 c 就会被重新命名成其他的名字。实际上,此时通常都会重新进行命名。清单 15 是对这个程序进行宏转换的一种可能的结果:

清单 15. 值交换宏的一种可能转换结果
(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 的宏可以提供其他宏系统的优点,却没有那些系统的问题。

然而,有时您可能会希望宏不是 hygienic 的。例如,可能希望在那些正在转换的代码中绑定这个宏。简单地声明一个变量并不能实现这种功能,因为 syntax-rules 系统会对变量重新进行命名。因此,大部分模式还包含一个非 hygienic 的宏系统,通常称为 syntax-case

syntax-case 宏很难编写,但是其功能更加强大,因为这样就可以使用完整的 Scheme 系统功能来进行转换了。syntax-case 宏并不是实际的标准,但是它们在很多 Scheme 系统中都已经实现了。没有 syntax-case 宏的系统通常也会有其他类似的系统可以使用。

让我们来看一下 syntax-case 宏的基本格式。让我们来定义一个宏 at-compile-time,它将在编译时执行一个给定的表单。

清单 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)))

它可以在编译时执行给定的操作。更具体地说,它是在宏展开时执行给定的操作,在 Scheme 系统中宏展开与编译并不总是同时进行的。Scheme 系统中编译时允许执行的任何表达式都可以在这个表达式中使用。现在让我们来看一下这是如何工作的。

使用 syntax-case 系统,实际上是在定义一个转换函数,这就是 lambda 发挥作用的地方。x 是正在转换的表达式。with-syntax 额外定义了一些语法元素,可以在转换表达式中使用。syntax 可以使用这些语法元素,并将其组合在一起,它遵循与 syntax-rules 中相同的转换规则。让我们来看一下每个步骤中会发生什么操作:

  1. at-compile-time 表达式匹配。
  2. 在转换最内部的地方,expression 被转换成一个列表,并作为普通的模式代码进行分析。
  3. 然后,结果与符号 quote 合并到一个列表中,这样 Scheme 就会在将其转换成代码时将其作为一个文本值进行处理。
  4. 这些数据被转换成一个 syntax 对象。
  5. 这个 syntax 对象在输出结果中使用名字 expression-value 表示。
  6. 转换器 (syntax expression-value) 认为 expression-value 是这个宏的全部输出。

利用这种在编译时执行计算的功能,我们可以编写一个比 C 语言更好的 TABLE 宏。清单 17 显示了在 Scheme 中应该如何使用 at-compile-time 宏:

清单 17. 在 Scheme 中构建平方根表
(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)

可以通过对这个宏进一步进行处理生成一个用来构建表的宏,进一步进行简化,这与前面的 C 语言版本的宏类似:

清单 18. 用来在编译时构建查找表的宏
(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)

现在,有了一个可以简单地构建任何想要的表的函数。

 

结束语

我们已经介绍了很多知识,因此现在花一分钟来回顾一下。首先我们讨论了哪些问题最适合使用代码生成程序来解决。这包括以下问题:

  • 需要提前生成数据表的程序
  • 有大量样板文件的程序,但是无法抽象成函数
  • 使用开发语言不具备的特性的程序

然后我们介绍了几种元编程系统,并给出了几个使用这些系统的例子。这包括通用文本替换系统,以及领域特有的程序和函数生成器。然后又介绍了一个具体的构建表的示例,并介绍了用 C 编写这样一个代码生成程序来构建静态表的详细过程。

最后,我们介绍了 Scheme,并了解了它如何解决我们在 C 语言中所面对的问题:它使用了一些结构,而这些结构本身就是 Scheme 语言的一部分。Scheme 既是一种语言,又是一种代码生成语言。由于这些技术都已经构建到了语言本身中,因此很容易编写程序,并且不会碰到其他语言中所面临的问题。这样我们就可以为 Scheme 语言在代码生成器传统应用的地方简单地添加一些领域特有的扩展了。

本系列文章的第 2 部分将详细介绍如何编写 Scheme 宏,以及如何使用这些宏来极大地简化大型编程任务。

参考资料

学习

posted @ 2014-03-26 12:52  dai.sp  阅读(540)  评论(0编辑  收藏  举报