宏语言为何不受欢迎

5

人类用计算机处理文本主要是依赖宏语言以及一些专用的文本编辑器。事实上,早期的文本编辑器只提供基本的文本编辑功能,然后借助宏语言进行功能扩展。结果人类很快就发现,基于宏扩展的编辑器,功能越复杂,它的行为就越诡异。于是,文本编辑器的扩展语言很快就被换成了当时的一种通用的动态类型的函数式编程语言——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 支持中文,对于中文用户而言,长期以来一直是初学者遇到的第一个本来不应该是障碍的障碍。

滥用宏语言所提供的编程能力,所产生的问题往往要比它解决的问题更多。许多现代的编程语言已经不再提供宏机制,C++ 虽然支持 C 语言宏,但是它几乎不停的告诫程序员最好不要用宏,而是用 const 或内联函数。

可以用宏去薄层封装那些繁琐且需要多次重复使用的代码,但是原则上不要用宏去实现过于复杂的逻辑。

我定义了一个 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 的卫生宏可以用来包油条。


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

载入中...