## 遞歸
現在再強調一次,m4 會將當前宏的展開結果插入到待讀取的輸入流的前端。也就是說,m4 會對當前宏的展開結果再次進行掃描,以處理嵌套的宏調用。這個性質決定了可以寫出讓 m4 累死的遞歸宏。
例如:
~~~
define(`TEST', `TEST')
TEST
~~~
當 m4 試圖對?`TEST`?進行展開時,它就會永無休止的去展開?`TEST`,而每次展開的結果依然是?`TEST`。
既然有遞歸,那么就可以利用遞歸來做一些計算,為了讓遞歸能夠結束,這就需要 m4 能夠支持條件。幸好,我們已經知道 m4 是支持條件的。下面,是一個遞歸版本的 Fibonacci 宏的實現與應用,它可以產生第 47 個 Fibonacci 數:
~~~
divert(-1)
define(`FIB',
`ifelse(`$1', `0',
0,
`ifelse(`$1', `1',
1,
`eval(FIB(eval($1 - 1)) + FIB(eval($1-2)))')')')
divert(0)dnl
FIB(46)
~~~
m4 的展開結果應該是?`1836311903`。也許你要等很久才會看到這個結果。因為遞歸的 Fibonacci 數計算過程中包含著大量的重復計算,效率很低。
不過,迭代版本的 Fibonacci 數計算過程也能寫得出來:
~~~
divert(-1)
define(`FIB_ITER',
`ifelse(`$3', 0,
$2,
`FIB_ITER(eval($1 + $2), $1, eval($3 - 1))')')
define(`FIB', `FIB_ITER(1, 0, $1)')
divert(0)dnl
FIB(46)
~~~
迭代計算很快,在我的機器上只需要 0.002 秒就可以得出?`1836311903`?這個結果。不過,如果想嘗試比 46 更大的數,比如`FIB(47)`,結果就會出現負數。這是因為 m4 目前只支持 32 位的有符號整數,它能表示的最大正整數是 2^31 - 1,而`FIB(47)`?的結果會大于這個數。
## 循環
既然有遞歸,那么就可以用它來模擬循環。例如:
~~~
define(`for',
`ifelse($#,
0,
``$0'',
`ifelse(eval($2 <= $3),
1,
`pushdef(`$1',$2)$4`'popdef(`$1')$0(`$1', incr($2), $3, `$4')')')')
~~~
這個?`for`?宏可以像下面這樣調用:
~~~
for(`i', 1, n, `循環內的計算')
~~~
它類似于 C 語言中的?`for`?循環:
~~~
for(int i = 1; i <= n; i++) {
循環內的計算
}
~~~
例如,可以用?`for`?宏將 64 個?`-`?符號發送到輸出流:
~~~
for(`i', 1, 64, `-')
~~~
這個宏的展開結果為:
~~~
----------------------------------------------------------------
~~~
如果你用過 reStructuredText 標記語言,一定會知道怎么用?`for`?宏構建一個協助你構造一個用于快速撰寫 reStructuredText 標題標記的宏。
要理解?`for`?宏的定義,有幾個 m4 小知識需要補習一下。請向下看。
## 宏參數列表的特征值
我們已經知道?`$1, $2, ..., $9`?對應于宏參數列表中的各個參數(GNU m4 不限定參數的個數,其他 m4 實現最多支持 9 個參數)。如果對 C 或 Bash 有所了解,那么當我說?`$0`?是宏本身,估計不會覺得很奇怪。因此,在上一節?`for`?宏定義中,`$0`?表示引用了宏名?`for`。不妨將?`$0`?改成?`for`?試一下。
`$#`?表示宏參數的個數。例如:
~~~
define(`count', ``$0': $# args')
count # -> count: 0 args
count() # -> count: 1 args
count(1) # -> count: 1 args
count(1,) # -> count: 2 args
~~~
> `#`?是注釋符,`->`?后面的文本是 m4 對注釋符號之前的文本處理后發送到輸出流的結果。
值得注意的是,即使?`()`?內什么也沒有,m4 也會認為?`count`?宏是有一個參數的,它是空字串。
`for`?的定義中,第一處條件語句為:
~~~
ifelse($#,
0,
``$0'',
... ...)
~~~
它的作用就是告訴 m4,遇到?`for`?的調用語句,如果?`for`?的參數個數為 0,那么?`for`?的展開結果為帶引號的字符串:
~~~
`for'
~~~
要理解為什么在條件語句中,`for`?用兩重引號包圍起來,你需要再認真的復習一次 m4 的宏展開過程。如果用單重引號,那么以無參數的形式調用?`for`?宏時,m4 會陷入對?`for`?宏無限次的展開過程中。
## 宏的作用域
所有的宏都是全局的。
如果我們需要『局部宏』該怎么做?也就是說,如何將一個宏只在另一個宏的定義中使用?局部宏的意義就類似于編程語言中的局部變量,如果沒有局部宏,那么在一個全局的空間中,很容易出現宏名沖突,導致宏被意外的重定義了。
為了避免宏名沖突,一種可選的方法是在宏名之前加前綴,比如使用?`local`?作為局部宏名的前綴。不過,這種方法對于遞歸宏無效。更好的方法是用棧。
m4 實際上是用一個棧來維護宏的定義的。當前宏的定義位于棧頂。使用?`pushdef`?可以將一個臨時定義的宏壓入棧中,在利用完這個臨時的宏之后,再用?`popdef`?將其彈出棧外。例如:
~~~
define(`USED',1)
define(`proc',
`pushdef(`USED',10)pushdef(`UNUSED',20)dnl
`'`USED' = USED, `UNUSED' = UNUSED`'dnl
`'popdef(`USED',`UNUSED')')
proc # -> USED = 10, UNUSED = 20
USED # -> 1
~~~
如果被壓入棧的宏是未定義的宏,那么?`pushdef`?就相當于?`define`。如果?`popdef`?彈出的宏也是未定義的宏,`popdef`?就相當于?`undefine`,它不會產生任何抱怨。
GNU m4 認為?`define(X, Y)`?與?`popdef(X)pushdef(X, Y)`?等價。其他的 m4 實現會認為?`define(X)`?等價于`undefine(X)define(X, Y)`,也就是說,新的宏的定義會更新整個棧。?`undefine(X)`?就是取消?`X`?宏的定義,使之成為未定義的宏。
## 讓宏名更安全
m4 有一個?`-P`?選項,它可以強制性的在其內建宏名之前冠以?`m4_`?前綴。例如下面的 M1.m4 文件:
~~~
define(`M1',`text1')M1 # -> define(M1,text1)M1
m4_define(`M1',`text1')M1 # -> text1
~~~
直接用 m4 處理,結果為:
~~~
$ m4 M1.m4
text1 # -> define(M1,text1)M1
m4_define(M1,text1)text1 # -> text1
~~~
如果用?`m4 -P`?來處理,結果為:
~~~
$ m4 -P test.m4
define(M1,text1)M1 # -> define(M1,text1)M1
text1 # -> text1
~~~
## 挑戰
理解?`for`?宏的定義。