話題:#科技# #數學# #計算機語言# #LaTeX#
小石頭/編
TeX在原子命令的執行上是嚴格按照順序的,但是宏的展開卻允許在順序的基礎上進行微調,這樣做的因為是:宏展開相當于程序員編寫代碼的過程,對于程序員來說,只能是大部分時間按順序coding,但有時會返回修改或暫時跳過。
在前面章節中,我們已經接觸過二種調節展開順序的用例,
保證尾遞歸:defcs{ ... el ... expandafter cs fi}
通過 expandafter 使得 fi 先展開,而 fi 展開為空,于是 cs 就到了自己的尾部。
阻止定義時(高速模式)展開: oks77={cmd}edefcs{
oexpand cmd he oks77}
命令 oexpand 會阻止 cmd 在cs的定義時展開, he會取出 oks77的內容cmd,但不會做進一步展開。
命令執行優先級,其實也需要改變了 執行順序,考慮如下命令復合代碼,
defa#1.{a[#1]} def#1.{b(#1)}a x.. % => a[b(x])
輸出結果出錯了!這顯然不是命令復合的本意,我們想要的結果是 ①
a[b(x)]
分析原因:第二行,由于 a 先被執行 所以 x 成了它的參數,于是展開為(注意:命令會吃的后面的空格),
a[ x].
接著被展開,x]匹配為它的參數,于是最終展開為,
a[b(x])
要修復這個BUG就要 先于 a 展開。這在其它 語言里,可以使用 括號 來調整表達式的優先級,但是TeX沒有這個機制。不過可以使用 分組來 替代,第二行可改為,
a { x.}.
這次在a展開時,參數變成了一個分組 { x.},于是展開為,
a[{ x.}]
然后是再以x為參數展開,得到 ②,
a[{b(x)}]
最后,執行展開結果,分組符號不會輸出,最終結果為就是①了。
這里必須注意,②和① 是有區別的,也即是說,在使用 分組調節優先級的同時引入了分組。若想要展開的直接結果就是 ① ,可以這樣,
expandafter a x..
命令 expandafter 保持 先于 a 被展開一次,它匹配參數是 x,于是展開為,
a b(x).
接著,a再以 b(x) 為參數,展開就是 ①了。
在一般情況下 ① 和 ② 并差別,可是在 let 命令中,就會不同。考慮用 csname命令來產生命令名,例如:
letcsname zeroendcsname=0 % ! Extra endcsname.
出錯了!原因是 let 將 csname 當成了要復制的命令,而由于 = 可是省略,所有實際上是將 字符 z 賦值給了 csname,之后 ero 不展開,然后到endcsname 由于找不到 與之匹配的 csname 于是就多了出來 。
解決這個問題,就是要讓 csname 先于 let 被執行。此時如果使用 分組,
let{csname zeroendcsname}=0 % ! Missing control quence inrted.
則不行了,原因是分組并不是命令不能被賦值。而使用 expandafter,
expandafterletcsname zeroendcsname=0zero % => 0
就沒有問題了!
def 與 let 類似,例如:
defcsname zeroendcsname{0}zero % ! Undefined control quence.
出錯了,原因是 第一行等于 重新定義了 csname。解決這個問題,同樣需要使用 expandafter,
expandafterdefcsname zeroendcsname{0}zero % ! % => 0
OK!
直到現在,我們使用宏來生產代碼都是,句法級別的,也就是以 標記(token)為單位的,而宏還可以深入到,詞法級別,也就是可以分拆和組合命令名。
分拆命令名:TeX 提供 命令 string 將 命令 轉為 命令名字符串,例如:
string cs % => cs
但是之前會帶有 轉義符 ,這時可以寫一個 單參數 空命令,吃掉它,
expandafter ail string cs % => cs
然后,將這個功能包成一個宏,
defuncsname#1{expandafter ailstring #1}uncsnamecs % => cs
我們就完成了分拆命令名的第一步,取出命令名。
接下就可以使用帶定界符的宏進行模式匹配,進一步分析命令名。
組合命令名:這個我們已經知道了就是 csname 命令,有三類對象和相互組合:
⑴ 立即字符:
defvalue{abc}expandafterletcsname abcendcsname=value
⑵ 宏:
def
ame{b}expandafterletcsname a
ame cendcsname=value
⑶ 標記列寄存器:
oks77={b}expandafterletcsname a he oks77 cendcsname=value
注:宏命令 char n 可以產生單個編碼是n的字符,而 ^^n 可產生編碼是 n±127 的字符。
好了,關于詞法宏,的機制也就這么多了,下面舉個栗子。
考慮,前文提到過的, TeX 生成標記的命令,
ewififabc
實際上,TeX僅僅為我們添加如下兩行代碼 ③,
defabctrue{letifabc=iftrue}defabcfal{letifabc=iffal}
這 功能上很簡單,因此要實現 ewif 命令的關鍵是 從 ifabc 命令名中 解析出 abc ,再由 abc 組成出abctrue、abcfal 和 ifabc 三個命令。實現分析:
解析出abc,考慮 使用 uncmd 可從 ifabc 先 中 解析出 ifabc,然后再使用 兩次 tail 分別吃掉 if 就剩下了 abc,具體代碼如下:
edefflag{uncsnameifabc}edefflag{expandafter ailflag} edefflag{expandafter ailflag}flag % => abc
值得一提:由于 expandafter 也是宏,于是可以被自己作用自己 用以達到 排序效果,例如上面的第二、三行代碼可以改寫為:
edefflag{expandafterexpandafterexpandafter ailexpandafter ailflag}
是不是很酷!
定義命令,這個并不難,但要搞清,定義時 和運行時,(以 abctrue 為例)代碼如下:
expandafteredefcsnameflag trueendcsname{
oexpandletcsname ifflagendcsname=
oexpandiftrue}
試一試,
* showabctrue> abctrue=macro:->let ifabc =iftrue
和 ③ 處一摸一樣,OK!
綜上, ewif 的最終實現為,
def
ewif#1{ edefflag{unwrap #1} edefflag{expandafter ailflag} edefflag{expandafter ailflag} expandafteredefcsnameflag trueendcsname{
oexpandletcsname ifflagendcsname=
oexpandiftrue} expandafteredefcsnameflag falendcsname{
oexpandletcsname ifflagendcsname=
oexpandiffal} abcfal}
試一試,
ewififabcifabc abc true el abc fal % => abc falfiabctrueifabc abc true % => abc trueel abc falfi
OK!
這詞法宏看著不難,但是用起來很容易出來,大家使用時一定要謹慎。
在任何語言的使用的討論中,數據結構都是不可避免的話題,而列表又是數據結構的核心之一。前面討論過 逗號分隔 的 整數列,這里在我們談談 TeX 代碼本身,即,標記列,標記列寄存器本來就是用來保存它的,它才是 TeX 的第一等公民,接下來,如果無特殊說明,所謂 列表 就是指它。
借助于前面討論過的 edef + he 的特性,很容易將兩個 標記列寄存器 合并起來,放在一個列表宏里,
edefc{ he oks0 he oks1}
于是要合并兩個列表宏,只需要在這之前,將其內容保存在 這兩個標記列寄存器中,
oks0=expandafter{a} oks0=expandafter{}
其中 expandafter 確保 a 被展開,而非以 命令的 形式 呆在 列表中。在這里標記列寄存器僅僅是臨時被使用,我們的列表最終都保持在宏中。
將以上代碼封裝起來就是,
defconcat#1=#2{ oks0=expandafter{#2} oks1=expandafter{#3} edef#1{ he oks0 he oks1}}
測試一下,
edefa{a} edef{b}concatc=a& c % => ab
OK!
接著就是列表中往添加元素問題。之前的整數列 的元素 之間 是用 逗號分隔的,即,
n1, n2, n3
而這次的是標記列,每個元素item都是(一個或多個)標記,所以應該用 分組來分隔,然后為了將來通過宏參數的模式匹配來處理列表,還需要在每個分組前面再加上@用來定界,因此我們的列表元素是這樣的,
@{item1}@{item2}@{item3}
這樣以來,在列表末尾 添加元素的具體代碼如下,
defappend#1 o#2{ oks0={@{#1}} oks1=expandafter{#2} edef#2{ he oks1 he oks0}}
測試測試,
letlst=emptyappend a olst append b olst append c olst showlst % -> @{a}@{b}@{c}.
OK! 當然,也允許向列表頭添加元素,代碼如下,
defunshift#1 o#2{ oks0={@@{#1}} oks1=expandafter{#2} edef#2{ he oks0 he oks1}}
測試一下,
unshift 0 olstshowlst % -> @{0}@{a}@{b}@{c}.
OK!
然后,就是取出列表的首項了,代碼如下,
defshift#1 o#2{ defperformshift@##1##2 o##3##4{def##3{##1} def##4{##2}} expandafterperformshift#1 o#2#1}
試一試,
shift lst oshow % -> 0.showlst % ->@{a}@{b}@{c}.
OK!
最后,就是對列表中元素的處理問題了。在之前的整數列中,我們使用了函數式語言的傳統方法,即,
把整數列中的每個元素取出來進行處理,然后再將處理結果組成新的整數列 ;但這里的 標記列,其實就是 TeX 代碼,它本身就可以運行的,如果我們用 而我們特意使用了 宏命令 @ 作為 分界符,它索然在參數匹配中不起作用,但是如果 列表去運行,就會被執行,不就相當于獲得了 處理 對每個 元素的能力嗎?
按照這個思路,我們只需要,將處理動作提前封裝在 @, 然后讓列表宏 執行,就可以了處理列表元素了。例如,計算列表的長度,
deflength#1 o#2{ #2=0 def@##1{advance #2 by 1} #1}
測試一下,
showlst % ->@{a}@{b}@{c}.lengthlst oeax % => 空showtheeax % -> 3.
完美!
將列表輸出成我們熟悉的寫法非常容易,
def helist#1{ eax=0 def@##1{ advance eax by 1 ifnum eax=1 el , fi ##1} #1} helistlst % => a,b,c
OK!
實現map功能同樣非常容易,
defmap#1y#2 o#3{ let#3=empty def@##1{ edefmaptemp{#2##1} expandafterappend maptemp o#3} #1}
測試一下,
defuc#1{ if a#1 A el if b#1 B el if c#1 C el ... fififi}maplstyuc oashowa % ->@{ A }@{ B }@{ C }.
OK!
注意:由于這個map的實現是,在 定義時 調用 #2 的,所以 #2 中的實現不能使用 非運行時 的原子命令,比如 advance 等。
(好了,續三就寫到這里吧!關于 TeX編程的更多內容,后續文章再討論!)
本文發布于:2023-02-28 21:21:00,感謝您對本站的認可!
本文鏈接:http://www.newhan.cn/zhishi/a/1677750251110964.html
版權聲明:本站內容均來自互聯網,僅供演示用,請勿用于商業和其他非法用途。如果侵犯了您的權益請與我們聯系,我們將在24小時內刪除。
本文word下載地址:個性分組符號(個性分組符號怎么打).doc
本文 PDF 下載地址:個性分組符號(個性分組符號怎么打).pdf
| 留言與評論(共有 0 條評論) |