眾所周知,在 TeX 中定義宏時,宏參數可以使用 tokens 來定界,從而減少宏層面的判斷(如 \if 、\ifx 、\strcmp ),以此加快編譯時間,LaTeX3 源碼中就包含大量的此類技巧,諸如 map 和 break 中:
\cs_new:Npn \prg_map_break:Nn #1#2#3 \prg_break_point:Nn #4#5 { #5 \if_meaning:w #1 #4 \exp_after:wN \use_iii:nnn \fi: \prg_map_break:Nn #1 {#2} }\cs_new:Npn \tl_map_function:nN #1#2 { \__tl_map_function:Nnnnnnnnn #2 #1 \s__tl_stop \s__tl_stop \s__tl_stop \s__tl_stop \s__tl_stop \s__tl_stop \s__tl_stop \s__tl_stop \prg_break_point:Nn \tl_map_break: { } }\cs_new:Npn \clist_map_function:NN #1#2 { \clist_if_empty:NF #1 { \exp_after:wN \__clist_map_function:Nw \exp_after:wN #2 #1 , \s__clist_stop , \s__clist_stop , \s__clist_stop , \s__clist_stop , \s__clist_stop , \s__clist_stop , \s__clist_stop , \s__clist_stop , \prg_break_point:Nn \clist_map_break: { } } }
至於 \clist_use:Nnnn 、\seq_use:Nnnn 宏就更加複雜了。
哪怕是基本數據類型的實現中也是使用 tokens 來分隔的(如 seq 、prop 、fp )。
本文首先介紹一下 TeX 宏定義的方式,然後使用定界的參數這一技巧來實現一個可展開的解析方括號可選參數的宏,最後順帶講解一下 \prg_map_break:Nn 、\tl_map_function:NN 、\clist_use:Nnnn 這三個宏的實現方式。
下述的 TeX 均指包含 eTeX 擴展的 TeX(如 pdfTeX,XeTeX,LuaTeX,upTeX,pTeX-ng 等),不包括 LuaMetaTeX,LuaMetaTeX 增加了大量的用於編程的特性。將 delimited parameter 譯為「定界的參數」,undelimited parameter 譯為「非定界的參數」,儘管並不那麼貼切。類別碼(catcode)均使用 LaTeX 代碼中正常情況下的類別碼, @ 一般視為 letter,_ 、: 在相應的情況下視為 letter,空格和 endlinechar 在使用相應情況下的類別碼。其它字符的類別碼使用 LaTeX 默認的類別碼,這樣可以為本文省下些篇幅。
TeX 中聲明(assignment)一個宏是這樣的:
⟨macro assignment⟩ -> ⟨definition⟩ | ⟨prefix⟩ ⟨macro assignment⟩⟨prefix⟩ -> \global | \long | \outer | \protected⟨definition⟩ -> ⟨def⟩ ⟨control sequence⟩ ⟨definitio text⟩⟨def⟩ -> \def | \edef | \gdef | \xdef⟨definition text⟩ -> ⟨parameter text⟩ ⟨left brace⟩ ⟨balanced text⟩ ⟨right brace⟩
最簡單的一個宏聲明就是:\def\hello{Hello, LaTeX} ,它在當前組中定義了 \hello 宏,如果 \hello 在此聲明前已經存在,總是使用展開時最新的那次聲明的定義。
這裡的 \hello 就是一個 control sequence ,{Hello, LaTeX} 就是 ⟨definition text⟩ ,⟨paramerter text⟩ 為空,⟨balanced text⟩ 為 Hello, LaTeX 。本文中,我們尤其關心 ⟨parapeter text⟩ 。
如下代碼聲明了一個宏 \hello :
\def\hello#1{Hello, #1.}
有一個變量,它展開時需要吸收(absorb)一個變量,如 \hello{LaTeX} 將輸出 Hello, LaTeX. 。一個宏的參數最多可以有 9 個,它們必須按順序在 ⟨parameter text⟩ 中給出,⟨balanced text⟩ 中可以使用也可以不使用它們,出現的順序也可以是任意的、多次的,如:
\def\hellofamily #1#2#3#4#5#6#7#8#9{Hello, #2, #5, #9 of #5, #7 and #1.}
參數還可以使用一個或多個記號來分界,如:
\def\longitude #1.{\ensuremath{\textrm{lo: }#1^{\circ}} }\def\latitude #1.#2 {\ensuremath{\textrm{la: }#1.#2^{\circ}} }
定義了 \longitude ,它需要 1 個參數,且必須使用 . 來定界。即使用方式為 \longitude 123. ,它將輸出:lo: 123° 。
\latitude 需要 2 個參數,且第一個參數使用 . 定界,第二個參數使用 (空格)定界,如:\latitude 43.5 ,將輸出:la: 43.5° 。
如果變量中包含分界符,則需要使用一對括號將其保護起來:\longitude {123.45}. 。將輸出 lo: 123.45° 。
參數的定界符也可以是控制序列或者多個字符,如:
\def\foo #1.=#2-\a-{#1|#2}
parameter text 中的控制序列不會被展開,因此它們是否被定義是無關緊要的。
如下定義了 \coordinate ,它需要 3 個參數,第 1 個和第 3 個是未分界的,第 2 個參數使用空格分界。
\def\coordinate #1#2 #3{coordinate: #1, at (#2, #3)}
以下是幾種可能的寫法:
\coordinate {A}12 {89} \par\coordinate{A}12 {89} \par\coordinate A12 {89} \par\coordinate A12 89 \par\coordinate A 12 {89} \par
它們的效果如下:
可以看到前三種的效果相同,第四個似乎有點出乎意料了。
第一、第二使用上的區別是,{A} 與 \coordinate 有空格,但似乎不影響效果。其原因是 TeX 在掃描文本時將控制序列後的空格忽略了,因此在 TeX 看來它們都是 \coordinate{A}12 {89} 。
第一、第三個的區別是第一個變量一個有花括號一個沒有,但 TeX 似乎將它們視為相同的。其原因是宏的未分界參數在吸收變量時,如果它發現它正要吸收(take)的這個字符不是 { 時,它直接吸收(take)該字符,否則,TeX 將繼續往後搜索,直到遇到正確嵌套了的組,然後 TeX 將這個組剝去最外層的花括號,將其作為相應的參數。於是,在 TeX 看來,\coordinate {A} 就相當於 \coordinate A 。
第三、第四個的區別是,第三個變量一個有花括號一個沒有,但結果卻不一樣。原因和上方是相同的,\coordinate A12 {89} 第三個參數是 89 ,而 \coordinate A12 89 第三個參數是 8 ,因為不是 { 時,TeX 只吸收它正在掃描的那個字符,這裡也就是 8 ,參數掃描完畢後,TeX 把 A 、12 、8 作為其三個參數,輸出結果,而 8 後方的 9 則留在原地。
第三、第五的區別是,空格首先出現在 A 之後,而 A 被吸收為了第 1 個參數,TeX 正要吸收第 2 個參數,這個參數是由空格分界的,由於 TeX 在掃描定界的變量時,它總是使用最短的那個,如果吸收的變量是如下形式:{⟨nested tokens⟩} ,則移除最外層的花括號,將 ⟨nested tokens⟩ 作為其參數。本例中,最短的那個參數為空,於是第二個參數為空。然後再吸收第三個參數,為 1 ,後方的字符留在原地。
關於 parameter text 的基礎部分已經講完了,實際上宏聲明還有許多內容,但限於篇幅,本文不再展開。
接下來簡單介紹一下 ⟨prefix⟩ 和 ⟨def⟩ 。
當我們需要把宏定義中的文本首先進行展開時,可以使用 \edef ,它將完全展開替換文本。\gdef 將全局地聲明這個宏,\xdef 則是 \gdef 和 \edef 的混合體。
\global 前綴用於將緊跟的聲明設為全局的,\long 前綴表示參數可以包含 \par ,\outer 前綴聲明的宏不能作為宏的變量,\protected 前綴聲明的宏在要需要被展開的記號列中不被展開,如:
\begingroup\gdef\name{Kunth}\protected\gdef\computer #1{Computer Sciencist: #1}\xdef\prize{\computer{\name}}\def\computer#1{computer sciencist: #1}\def\name{david}\prize\endgroup\prize
將輸出:
本文需要的基礎知識大體已經講完了,下面進入實踐階段!
接下來的代碼中,將 @ 看作字母。
有時候,我們需要使用可選參數,即這個參數可以不給出,如果沒有給出,則使用默認值代替,LaTeX 中常用的是方括號作為可選參數的分隔符:\foo[opt]{val} ,LaTeX 內核中大部分都是使用 \@ifnextchar 宏來實現的,這個宏基於 \futurelet primitive,它是不可展開的,因此在需要展開時則不能使用。
而 cmd ( xparse )則提供了更好的接口來實現這一需求,但是由它定義的宏內部十分複雜,對於需要非常多次使用的情況則編譯會非常慢。因此實現一個簡易的解析可選參數的宏是有必要的。
方括號定界的可選參數有三種情況:[#1]{#2} 、{#1}[#2] 、[#1]{#2}[#3] ,即首、尾、首尾。
首先來考慮第二種情況,即尾部是可選參數。例如,考慮這樣一個列表:
{ OE[2]=O, HF[-1], LCR=C, MN[4], IHTU=U,}
方括號中給出了對應鍵的數字索引的位置,如 OE[2] 即表示它應該出現在所有小於2和所有大於2的中間。那些沒有給出可選參數的,默認值就是上一個鍵的數字索引+1。即
{ OE[2], HF[-1], LCR[0], MN[4], IHTU[5] }
這就是一種常見情況。
這樣一個解析命令僅使用本文中介紹的內容即可完成。
\makeatletter\long\def\CusSplitTailDefault #1#2% tl, [default] {\Cus@split@tail@a #1\q@mark [{#2}]\q@mark \q@stop}\long\def\Cus@split@tail@a #1#2[#3]\q@mark #4\q@stop {\Cus@split@tail@b {#3}#1#2\q@mark \q@stop}\long\def\Cus@split@tail@b #1#2\q@mark #3\q@stop {\unexpanded{{#2}[{#1}]}}\makeatother\long\def\CusSplitTail #1{\CusSplitTailDefault {#1}{}}
核心代碼只有第 2-6 行,即實現了一個簡易的可選參數解析宏,它只需3 次展開(或4次,如果將 \unexpanded 也考慮進入)。
\CusSplitTailDefault 為接口,其中第一個參數為要解析記號列表,第二個參數為可選參數的默認值。
下圖為宏 \CusSplitTail 的效果,它將默認值設為空。13 種情況僅有一種「失效」(當然方括號的嵌套是無法用這幾行代碼解決的,但是通常情況已經夠用了)。
首先來看這個宏
\long\def\CusSplitTailDefault #1#2% tl, [default] {\Cus@split@tail@a #1\q@mark [{#2}]\q@mark \q@stop}
如果 #1 中包含可選參數 [..] 則展開結果為:
\Cus@split@tail@a ...[..]\q@mark [{#2]]\q@mark \q@stop
而 \Cus@split@tail@a 的定義可以是這樣的:
\long\def\Cus@split@tail@a #1[#2]\q@mark #3\q@stop
這樣要解析的記號列的可選參數就是 #2 、必須參數就是 #1 。
當要解析的記號列中沒有可選參數是,則展開結果為:
\Cus@split@tail@a ...\q@mark [{#2}]\q@mark \q@stop
同樣的可選參數的默認值就變成了 #2 ,而 #1 就是必須參數加上一個 \q@mark 。
但是,這樣的宏 \Cus@split@tail@a 有一個問題,就是加入要解析的參數為 [...] ,我們不希望這樣解析出來的必須參數為空,而可選參數為 ... ,因為至少必須參數的優先級高於可選參數,必須參數應該被優先提取。
解決方法也很簡單,我們只要讓 \Cus@split@tail@a 的第一個參數是非定界的即可,這樣它總會先提取一個值,保證其不為空,對於 [...] 的情況,因為 [ 已經被提取出了,它已經不滿足可選參數的定界條件了,只能向後尋找:
\long\def\Cus@split@tail@a #1#2[#3]\q@mark #4\q@stop
現在,經過上方的分析,我們已經確定無疑的是 #3 是我們需要的可選參數了,如果可選參數未在記號列中給出,那麼它等於默認值,否則,它就是我們給出的可選參數。
#1#2 中就包含我們需要的必須參數,但是從上面的分析中可知,它尾部還可能有一個 \q@mark ,我們需要去掉它。
\long\def\Cus@split@tail@b #1#2\q@mark #3\q@stop {\unexpanded{{#2}[{#1}]}}
如果上面的 #1#2 的尾部有 \q@mark ,那麼宏 \Cus@split@tail@b 將使用這個 \q@mark 作為定界符,否則,我們需要為它補一個 \q@mark 。那麼 \Cus@split@tail@a 的完整定義如下:
\long\def\Cus@split@tail@a #1#2[#3]\q@mark #4\q@stop {\Cus@split@tail@b {#3}#1#2\q@mark \q@stop}
至於 \Cus@split@tail@b 的定義,我們已經知道了如上的 #3 是需要的可選參數,而必須參數是用 \q@mark 分隔的,那麼 \Cus@split@tail@b 的定義就是顯而易見的了。
最後再來匯總一下這 5 行代碼:
\long\def\CusSplitTailDefault #1#2% tl, [default] {\Cus@split@tail@a #1\q@mark [{#2}]\q@mark \q@stop} % 將剝去最外層花括號,如果有的話\long\def\Cus@split@tail@a #1#2[#3]\q@mark #4\q@stop {\Cus@split@tail@b {#3}#1#2\q@mark \q@stop}% 將剝去花括號\long\def\Cus@split@tail@b #1#2\q@mark #3\q@stop {\unexpanded{{#2}[{#1}]}}% 剝去花括號
失效的那種情況也已經在上文中敘述了,即在解析的過程中剝去了兩層花括號。
下面直接給出解析首部可選參數的宏吧:
\long\def\CusSplitHeadDefault #1#2% [default], tl {\Cus@split@head@a \q@mark #2\q@mark [{#1}]#2\q@stop}\long\def\Cus@split@head@a #1\q@mark[#2]#3\q@stop {\Cus@split@head@b {#2}#3\q@mark \q@stop}\long\def\Cus@split@head@b #1#2\q@mark #3\q@stop {\unexpanded{[{#1}]{#2}}}\long\def\CusSplitHead {\CusSplitHeadDefault {}}
同樣只需 3 次展開(或4次,如果將 \unexpanded 也考慮進入)。
讀者可以自行分析一下這個宏。
13 種可能情況均通過,包括第 9 種,雙可選參數:
listings 宏包中 keywords 就使用這種語法,但它的缺點是必須使用花括號將必須參數(list of keywords)括起來,而使用這個解析宏可以直接寫 [number]list of keywords 而不必使用 [number]{list of keywords} 。由於第 9 種情況也能正確處理,因此像 index 鍵有兩個可選參數 [⟨number⟩][⟨keyword classes⟩]{⟨identifiers⟩} 也能正確處理。
下面這個宏用於解析首尾可選參數情況,需要多一次展開操作。
\long\def\CusSplitOuterDefault #1#2#3% [default1], tl, [default2] {\Cus@split@outer@a\q@mark#2\q@mark[{#1}]#2\q@mark[{#3}]\q@mark\q@stop}\long\def\Cus@split@outer@a #1\q@mark[#2]#3[#4]\q@mark#5\q@stop {\Cus@split@outer@b{#2}#3\q@mark\q@nil[#4][#4]#5\q@mark\q@stop}\long\def\Cus@split@outer@b #1#2\q@mark#3\q@nil#4][#5]\q@mark#6\q@stop {\Cus@split@outer@c{#1}{#2}[#5]\q@mark[#5]\q@stop}\long\def\Cus@split@outer@c #1#2#3\q@mark[#4]#5\q@stop{\unexpanded{[{#1}]{#2}[{#4}]}}\long\def\CusSplitOuter #1{\CusSplitOuterDefault {}{#1}{}}
這個宏相較於上兩個宏僅多了兩行,但複雜得多,讀者可以自行分析一下。13 種可能情況也僅有一種「失效」,特別要注意第 5 種情況。
這三個宏可能並非解析可選參數的最好實現方式,讀者可以嘗試優化。在解析較短的記號列時,這三個宏比用 \NewExpandableDocumentCommand 定義的宏快了20倍,儘管功能有些簡陋。
最後來分析一下 \tl_map_function:nN 、\clist_use:Nnnn 的實現。
\cs_new:Npn \tl_map_function:nN #1#2 { \__tl_map_function:Nnnnnnnnn #2 #1 \s__tl_stop \s__tl_stop \s__tl_stop \s__tl_stop \s__tl_stop \s__tl_stop \s__tl_stop \s__tl_stop \prg_break_point:Nn \tl_map_break: { } }\cs_new:Npn \__tl_map_function:Nnnnnnnnn #1#2#3#4#5#6#7#8#9 { \__tl_use_none_delimit_by_s_stop:w #9 \__tl_map_function_end:w \s__tl_stop #1 {#2} #1 {#3} #1 {#4} #1 {#5} #1 {#6} #1 {#7} #1 {#8} #1 {#9} \__tl_map_function:Nnnnnnnnn #1 }\cs_new:Npn \__tl_map_function_end:w \s__tl_stop #1#2 { \__tl_use_none_delimit_by_s_stop:w #2 \tl_map_break: \s__tl_stop #1 {#2} \__tl_map_function_end:w \s__tl_stop }\cs_new:Npn \__tl_use_none_delimit_by_s_stop:w #1 \s__tl_stop { }
\tl_map_function:nN 第一個參數為要 map 的記號列,第二個為 apply 的宏。
當 #1 有充足的項時,\s__tl_stop 不會作為 \__tl_map_function:Nnnnnnnnn 的變量,此時要 apply 的宏作為 \__tl_map_function:Nnnnnnnnn 的第一個變量,第 2-9 分別為記號列中的前一部分,且第 9 個參數不為 \s__tl_stop 。
此時當 \__tl_map_function:Nnnnnnnnn 展開時,\__tl_use_none_delimit_by_s_stop:w 將吸收 #9 \__tl_map_function_end:w 作為其參數,它展開為空。接着將 apply 宏順序依次應用至這 2-9 個參數。
當這 8 個記號中包含有 \tl_map_break:n 且它恰好被展開時,\tl_map_break:n 將匹配到最近的一個 \prg_break_point:Nn ,並使用它作為 \prg_map_break:Nn 第三個參數的定界符,如果這個 \prg_break_point:Nn 後緊跟的是 \tl_map_break: ,這就是 \tl_map_function:nN 中的那行,\tl_map_function:nN 將終止。
否則,這 8 個記號中不包含 \tl_map_break:n 或未被展開,則繼續展開後面的結果。然後在尾部調用同樣的這個宏(尾遞歸),將 apply 宏作為其第一個參數,繼續吸收需要的另外 8 個參數。
重複 1-4 的過程,直到原記號列中沒有更多的項,\__tl_map_function:Nnnnnnnn 必須吸收 \s__tl_stop 作為其參數,此時它的第 9 個參數必為 \s__tl_stop 。
在展開 \__tl_map_function:Nnnnnnnn 時,第 9 個參數為 \s__tl_stop ,\__tl_use_none_delimit_by_s_stop:w 將這個 \s__tl_stop 作為其定界符,它展開為空。\__tl_map_function_end:w 被保留了下來。
進入掃尾階段,展開 \__tl_map_function_end:w ,它吸收 \s__tl_stop ,並吸收兩個參數,第一個為 apply 宏,第二個為原記號列的一項或為 \s__tl_stop 。
如果第二個變量不為 \s__tl_stop ,則 \__tl_use_none_delimit_by_s_stop:w 使用 \tl_map_break: 之後的那個 \s__tl_stop 作為參數定界符,展開為空。將 apply 宏應用到這個項上。
如果上述展開結果中有 \tl_map_break:n 時,\tl_map_break:n 將匹配到最近的一個 \prg_break_point:Nn ,並使用它作為 \prg_map_break:Nn 第三個參數的定界符,如果這個 \prg_break_point:Nn 後緊跟的是 \tl_map_break: ,這就是 \tl_map_function:nN 中的那行,\tl_map_function:nN 將終止。
否則,再次展開 \__tl_map_function_end:w ,吸收 \s__tl_stop ,並吸收兩個參數,第一個為 apply 宏,第二個為原記號列的一項或為 \s__tl_stop 。繼續執行 8-9。
直到第二個變量為 \s__tl_stop 。此時 \__tl_use_none_delimit_by_s_stop:w 使用這個 \s__tl_stop 作為參數定界符,它展開為空。
繼續展開 \tl_map_break: ,它匹配到 \tl_map_function:nN 宏中的那行 \prg_break_point:Nn \tl_map_break: ,整個 map 結束。
LaTeX3 中 map 的實現是十分巧妙的,在判斷是否為 \s__tl_stop 時,完全沒有使用\if宏,值得學習。
下面來分析 \clist_use:Nnnn 的實現,儘管代碼行數不多,但這個比上一個更加複雜。
\cs_new:Npn \clist_use:Nnnn #1#2#3#4 { \clist_if_exist:NTF #1 { \int_case:nnF { \clist_count:N #1 } { { 0 } { } { 1 } { \exp_after:wN \__clist_use:wwn #1 , , { } } { 2 } { \exp_after:wN \__clist_use:wwn #1 , {#2} } } { \exp_after:wN \__clist_use:nwwwwnwn \exp_after:wN { \exp_after:wN } #1 , \s__clist_mark , { \__clist_use:nwwwwnwn {#3} } \s__clist_mark , { \__clist_use:nwwn {#4} } \s__clist_stop { } } } { \msg_expandable_error:nnn { kernel } { bad-variable } {#1} } }\cs_new:Npn \__clist_use:wwn #1 , #2 , #3 { \exp_not:n { #1 #3 #2 } }\cs_new:Npn \__clist_use:nwwwwnwn #1#2 , #3 , #4 , #5 \s__clist_mark , #6#7 \s__clist_stop #8 { #6 {#3} , {#4} , #5 \s__clist_mark , {#6} #7 \s__clist_stop { #8 #1 #2 } }\cs_new:Npn \__clist_use:nwwn #1#2 , #3 \s__clist_stop #4 { \exp_not:n { #4 #1 #2 } }
首先判斷 clist 的項數,0、1、2 的情況很簡單,下面來看其它情況。(注意這一步會遍歷一次 clist)
將這種情況提取出來,只有如下幾行:
\exp_after:wN \__clist_use:nwwwwnwn\exp_after:wN { \exp_after:wN } #1 ,\s__clist_mark , { \__clist_use:nwwwwnwn {#3} }\s__clist_mark , { \__clist_use:nwwn {#4} }\s__clist_stop { }\cs_new:Npn \__clist_use:nwwwwnwn #1#2 , #3 , #4 , #5 \s__clist_mark , #6#7 \s__clist_stop #8 { #6 {#3} , {#4} , #5 \s__clist_mark , {#6} #7 \s__clist_stop { #8 #1 #2 } }\cs_new:Npn \__clist_use:nwwn #1#2 , #3 \s__clist_stop #4 { \exp_not:n { #4 #1 #2 } }
這裡的 #1 就是要 use 的那個 clist 宏,#3 為項中間要插入的代碼,#4 為最後兩項中要插入的代碼,即
i1 #3 i2 #3 i3 ... in-2 #3 in-1 #4 in
考慮最簡單的情況,即 #1 是僅有 3 項的宏,包含:1,2,3 。
前兩行,則是展開宏 #1 為 1,2,3 。於是前 5 行的結果為:
\__clist_use:nwwwwnwn { } 1,2,3,\s__clist_mark , { \__clist_use:nwwwwnwn {#3} }\s__clist_mark , { \__clist_use:nwwn {#4} }\s__clist_stop { }
為了簡便起見,我們將 \s__clist_mark 簡記為 m ,將 \s__clist_stop 簡記為 s ,將 #3 簡記為 i ,將 #4 簡記為 l ,將 \__clist_use:nwwwwnwn 簡記為 I ,將 \__clist_use:nwwn 簡記為 L 。
於是上述可以寫成:
I {} 1,2,3, m, { I i } m, { L l } s {}
I 第一個參數是非定界的,第2、3、4 使用 , 定界,第5 使用 m, 定界,6 是非定界的,7 使用 s 定界,8 是非定界的。
於是上述代碼的參數情況及展開為:
#1->#2->1#3->2#4->3 % 如果有更多項,均為第 4 個參數#5->#6->I i#7->m, { L l }#8->=>I i {2}, {3}, m, { I i } m, { L l } s { 1 }
可以發現經過這次展開首項移到了末尾,其它項均往前移了一位。這正是 \clist_use:Nnnn 的實現思路!逐項往後移動,並往後補 i ,直至進入掃尾階段。
上述結果再進行展開:
#1->i#2->2#3->3#4->m#5->I i#6->L l#7->#8->1=>L l {3}, {m}, I i m, { L l } s { 1 i 2 }
發現 i 和 2 被放到了 1 後邊,並且 L 作為第一個記號,這標誌着進入了掃尾階段。
展開 L :
#1->l#2->3#3->{m}, I i m, { L l } #4->1 i 2=>\exp_not:n { 1 i 2 l 3 }
於是便得到了我們想要的結果。更多的項也是類似的。
\clist_use:Nnnn 的實現非常巧妙,同樣也沒有使用 \if 宏。但是它的速度相較於 \clist_map_... 來說是較慢的。首先它必須 map 一次 clist,以得到它的項數,然後在每一次展開的過程中,TeX 都必須掃描所有項。
TeX 宏定義的技巧非常多,LaTeX3 源碼也是非常好的學習資料,當然在掌握這些技巧之前應該學好 TeX 的基礎知識,這方面,The TeXbook,TeX by Topic 是非常好的資料。
獲取下載文件