宏语言为什么不受欢迎

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

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

能够用宏去薄层封装那些繁琐且须要屡次重复使用的代码,可是原则上不要用宏去实现过于复杂的逻辑。code

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