《Lua-in-ConTeXt》02:ConTeXt 計算機

garfileo發表於2023-01-31
上一篇:Hello world

倘若計算機裡有個 ConTeXt 可以用,就可以認為它也是一臺計算機,是計算機裡的計算機。

用於編寫 TeX 原始檔(例如 hello.tex)的任何一種文字編輯器,都可視為「ConTeXt 計算機」的終端。context 命令可將 TeX 原始檔裡的內容輸出到 PDF 檔案,於是可將 PDF 檔案視為 ConTeXt 計算機的顯示器。

單有輸入和輸出的機器,還稱不上計算機,但是倘若在輸入裡能夠程式設計,那這個機器就肯定是計算機。我們可以在 TeX 原始檔裡程式設計,所用的程式語言最早是 TeX 語言。我以前寫過一篇沒打算寫完的文章「TeX 程式設計」(見本文附錄),其中的任何示例程式碼皆能基於以下格式的 TeX 原始檔由 context 命令解釋並執行

\starttext
這裡可以放任何示例程式碼
\stoptext

需要說明的是,含有 \starttext ... \stoptext 的 TeX 原始檔應當稱為 ConTeXt 原始檔。TeX 和 ConTeXt 的關係,類似於馬和馬車的關係。直接騎馬也是可以的,但是坐馬車會舒服很多。TeX 是 ConTeXt 的核心。像 ConTeXt 這樣的馬車還有一個,叫 LaTeX,也許聽說過它的人要比聽說過 ConTeXt 的人更多。

21 世紀了,現代人可以在 TeX 文件裡用 Lua 語言程式設計。Lua 語言本身並不依賴 ConTeXt,使用它可以為我們所熟悉的由一堆硬體組成的計算機編寫程式。當 ConTeXt 的開發者將 Lua 語言直譯器/編譯器嵌入到了 ConTeXt 計算機之後,使用 Lua 語言編寫程式時,可以使用 ConTeXt 計算機裡大量的資源,這意味著 Lua 語言得到了顯著強化。

例如,倘若使用 Lua 編寫一個可以生成 PDF 檔案的 Hello world 程式,在硬體架構的計算機裡,這樣的程式可能需要成百甚至上千行程式碼,而在 ConTeXt 計算機裡,只需要三行程式碼,例如:

\starttext
\ctxlua{context("Hello world!")}
\stoptext

這個程式與上一章裡的

\starttext
Hello world!
\stoptext

等價,但是前者出現了真正的 Lua 程式碼

context("Hello world!")

這行程式碼呼叫了 ConTeXt 計算機裡的 Lua 函式 context(注意,它不是 context 命令,二者僅僅是恰好同名而已),該函式將字串 "Hello world!" 輸出到了 TeX 檔案,然後 ConTeXt 計算機將 ConTeXt 原始檔的內容輸出至 PDF 檔案。

也許透過以下圖示,能夠理解上述的一切:

ConTeXt 計算機的輸入和輸出

現在,可以忘記 ConTeXt 是計算機了,它只是一個程式。我們與它的任何互動,就是透過一條非常簡單的 context 命令:

$ context 你的 ConTeXt 原始檔

不過,在一個使用著硬體體系的計算機使用者看來,Hardware the parts of a computer that can be kicked.(硬體,計算機中可剔除的部分。)


附錄:TeX 程式設計

此文大概寫於 2018 年 11 月 8 日。我已經忘記了當初為什麼沒有寫下去。以後可能也不會再寫下去。不過,有一個好訊息,我偶爾發現有人和我有同樣的想法,他寫了一份比下文更完善的 TeX 程式設計筆記

TeX 是一種面向文件排版的計算機程式語言,適用於處理科技文獻的排版任務,但本文幾乎不關心 TeX 的排版功能,僅從一門完備的程式語言應當具備的要素的角度去認識它。

常量

最基本的常量是單個字元,TeX 直譯器會照實對其予以解釋。例如

Hello world!

TeX 直譯器逐字照實輸出。輸出到何處?現代的 TeX 直譯器,諸如 pdftex、xetex、luatex 等,會將其輸出至 PDF 格式的文件。

常量之間只有一種運算,即連線。我們在輸入文字時,便已經實施了該運算——將一組字元連成文字。若將一組字元合成為一個常量,可以用 {...},例如

{Hello world!}

變數

變數可透過無引數的宏予以構造,例如透過 \def 構造一個變數並賦值:

\def\myvar{Hello world!}

\myvar 便是一個變數,它的值為 Hello world!。在 TeX 中,變數只有一種型別,即文字型別。

函式

函式即有引數的宏。我知道它應該叫作宏,但是不要改正我,在這篇文章裡我喜歡叫它函式。

函式與變數並沒有本質上的區別。所以,在數學中,變數也會被稱為常函式。

函式可以吸收常量或變數,將它們與其他常量或變數進行組合。例如,

\def\myfunc#1{#1 world!}

\myfunc 吸收了常量 Hello

\myfunc{Hello}

之後,就會將 Helloworld! 連線為 Hello world!{...} 可將一組字元常量合併為一個常量。在上例中,若不用 {...},而是直接

\myfunc Hello

結果得到的是「H world!ello」,因此 \myfunc 此時只吸收常量 H,剩下的 ello 只能等待與 \myfunc 的結果連線。

若將 Hello 作為值賦予一個變數,\myfunc 也能吸收這個變數:

\def\hello{Hello}
\myfunc\hello

由於函式與變數並沒有本質區別,所以函式也能吸收函式,例如:

\def\foobar#1{#1 {Hello}}
\foobar\myfunc

結果為「Hello world!」。

若一個函式將吸收到的量與這個函式自身進行組合,結果會導致 TeX 直譯器陷入到不停地解釋這個函式的過程,直至崩潰。例如

\def\foobar#1{#1\foobar{#1}}
\foobar{Hello world!}

在現實世界,類似這種形式的機器叫永動機。與 TeX 世界一樣,現實世界也造不出永動機。換言之,若現實世界能造出永動機,那麼在 TeX 世界一定也能。

\foobar 不吸收任何量,也不與任何量組合,即

\def\foobar{\foobar}
\foobar

在 TeX 的世界裡,它可以永動,然而它卻什麼都不能做了。像 \foobar 這樣的宏,在 TeX 中稱為遞迴宏……不是說好了嗎不叫宏的嗎?遞迴函式。

暫存器和條件

永動機雖然造不出來,讓一個函式自身與其所吸收的量進行組合,這種形式可以產生迴圈形式的動力。在現實世界,利用這種動力所取得的上天入地效果,我們都有所見識。在 TeX 世界裡也能如此,否則就不會有 LaTeX 和 ConTeXt 的出現。但是,要利用這種動力,就需要透過一些開關對其進行控制,否則這種動力便會摧毀整個 TeX 世界。

最簡單的開關是控制迴圈的次數,即控制一個函式自身與其所吸收的量進行組合的次數。這需要使用 TeX 的計數器。使用 \newcount 可以向 TeX 申請一個計數暫存器作為計數器,例如

\newcount\mycount

若讓這個計數器從 0 開始,只需

\mycount=0

若要控制函式自身與其所吸收的量進行組合的次數不大於 10 次,只需在該過程中增加控制語句

\ifnum\mycount=10
\else 函式自身與其所吸收的量的組合\advance\mycount by 1
\fi

例如

\def\foobar#1{
  \ifnum\number\mycount=10
  \else #1\advance\mycount by 1\foobar{#1}
  \fi
}

\newcount\mycount
\mycount=0
\foobar{Hello world!}

可將 Hello world! 分段輸出十次。

\newcount 的作用是分配一個未使用的計數暫存器,並賦予它一個名字。透過這個名字便可以使用這個計數暫存器中儲存的數值。Knuth 的 TeX 最多支援 256 個計數暫存器,現代的 TeX 對此進行了擴充套件,例如 LuaTeX 可支援 65536 個。可直接以數字為字尾的 \count 使用計數暫存器,例如

\def\foobar#1{
  \ifnum\number\count65534=10
  \else #1\par\advance\count65535 by 1\foobar{#1}
  \fi
}

\count65535=0
\foobar{Hello world!}

但是這樣做,很容易引起混亂。例如,倘若某種 TeX 格式將 \count65535 用於儲存某個重要的排版資料,這裡使用了這個暫存器,那麼這個暫存器中原有的值就會被覆蓋,可能會導致排版結果出現難以預測的結果。因此,通常推薦使用 \newcount 申請一個尚未被使用的暫存器。這裡需要糾正一下前文中的一個說法——TeX 的變數的型別僅有文字型別。事實上,透過 \newcount 構造的計數暫存器本質上是整數型別的變數。

\advance 用於整型變數的加減運算,例如對一個整型變數加 10,再減 30,再增加 1 倍:

\newcount\abc
\abc=0
\advance\abc by 10
\advance\abc by -30
\advance\abc by\abc
\the\abc

結果為 -40。\the 用於攫取整型變數的值。

事實上,TeX 變數的型別還有更多。除了計數暫存器,還有盒子(box)暫存器、維度(dimen)暫存器、skip 暫存器、musikip 暫存器以及 toks 暫存器,這些變數的值皆能用 \the 獲取。

\ifnum 用於比較兩個數值的關係,即大於、小於和等於。類似的條件語句還有

  • \iftrue 永遠為真,\iffalse 永遠為假;
  • \if:測試兩個字元是否相同;
  • \ifx:測試兩個記號(Token)是否相同;
  • \ifcat:測試兩個記號的類別碼是否相同;
  • \ifdim:比較兩個尺寸的關係;
  • \ifodd:測試一個數值是否為奇數;
  • ……更多的,見《The TeXbook》第 20 章 ……

這些條件語句,待需要使用它們之時再作細究。

尾遞迴

利用遞迴函式可以製作通用的迴圈語句,例如若製作類似於 TeX 的 \loop ...\repeat 的結構,只需

\def\myloop#1\repeat{\def\body{#1}\myiterate}
\def\myiterate{\body\myiterate\else\relax\fi}

\relax 是個什麼都不做的控制序列,將其刪除,對 \myloop 毫無影響,但是使用它可以讓 \myiterate 的定義更清晰。

現在,用 \myloop ...\repeat 結構將 Hello world! 輸出 10 次:

\newcount\mycount
\mycount=0
\myloop Hello world!\advance\mycount by 1\ifnum\mycount<9\repeat

現在來看 \myiterate 的定義……

未完……

另附

在 TeX 程式設計中,類別碼(Category Code)和記號(Token)是非常基礎的兩個概念。可透過 TeX 的作者 Donald Knuth 所寫的《The TeXbook》的第 7 章瞭解它們。

TeX 按行讀取文件中的字元。在該過程中,TeX 會對讀入的字元進行分類。在 TeX 看來,字元可分為 16 類,類的編號從 0 到 15。經 TeX 分類後的每個字元構成記號。此外,TeX 的控制序列也構成記號。因此,TeX 讀取文件的過程便是生成記號序列的過程,記號序列由字元記號和控制序列記號構成。

字元記號所屬的類別決定了 TeX 在讀入文件如何理解它們。例如,當 TeX 讀入字元 { 時,會將它歸為類 1,屬於這一類別的字元記號,TeX 會將其視為一個編組的開始符號。當 TeX 讀入字元 } 時,會將它歸為類 2,屬於這一類別的字元記號,TeX 會將其視為一個編組的結束符號。因此,當 TeX 讀入類似 {天地一指也,萬物一馬也。} 這樣的字元序列之後,會將 天地一指也,萬物一馬也。 視為一個編組。

對於一個字元,TeX 本身並不知道它應當歸於哪個類別。字元所屬類別需要由 TeX 的使用者透過控制序列 \catcode 予以設定,這個控制序列是 TeX 的原始控制序列。不過,在使用某種 TeX 格式排版時,該格式會對字元進行歸類,使用者只需承認這些歸類的合理性,然而心安理得地使用這種 TeX 格式完成排版任務。

TeX 的使用者有時也需要臨時地修改某些字元的類別。例如,現在有許多網站支援 TeX 數學公式,但是對於行內公式,這些網站往往會將公式文字放入 \(\) 之間,而不是放入 TeX 所沿襲的一對 $ 之間。若讓 TeX 也支援這種形式,只需將 () 的類別編碼修改為 11(字母類別),然後便可以定義 \(\) 宏`,之後再復原它們的類別編碼:

\catcode`(=11
\catcode`)=11
\def\({$}
\def\){$}
\catcode`(=12
\catcode`)=12

之後,在 TeX 文件裡便可以像下面這樣寫數學公式:

行內公式:\(E=mc^2\)

在 Markdown 中,\(\) 需要寫成 \\(\\)。不過,在幾乎所有的 TeX 格式中,\ 用作控制序列的開始記號,這樣就很難定義 \\(。對於這種情況,不妨先以 \(\) 代替 $,在此基礎上,利用文字編輯器的替換功能將 \(\) 替換為 \\(\\)。例如

$ sed -i 's/\\(/\\\\(/g; s/\\)/\\\\)/g' foo.tex

若將文件中的 \\(\\) 再復原為 \(\),只需

$ sed -i 's/\\\\(/\\\(/g; s/\\\\)/\\\)/g' foo.tex
下一篇:兩個世界

相關文章