《Lua in ConTeXt》10:學一點 Lua 變量 函數 表 Lua 的力量 條件 迭代 結語

沉緬於 ConTeXt 有些不能自拔,幾乎忘記了這份文檔的題目是 ConTeXt 裏的 Lua。主角應該是 Lua,ConTeXt 只是定語。但是,這個定語實在太長,遷延至今,這份文檔引入的 ConTeXt 知識尚不及 ConTeXt 全部知識的 1%。

之前,我有兩三次在 ConTeXt 源文件裏寫了一些 Lua 代碼,可將這些舉動視爲熱身。未見其容,先聞其聲,我覺得應該比捧起《Programming in Lua》學習 Lua 更有效。現在到了對 Lua 的真容有所見識的時候了。

變量

在 card-env.tex 文件裏,有以下 Lua 代碼片段:

mingyi = {}

local ctx = context
local dim = number.todimen
local w = tex.dimen.textwidth
local w1, w2, w3 = 0.0655 * w, nil, 0.05 * w
w2 = w - (w1 + w3)

其中,mingyictxdimww1w2, w3,皆爲變量。mingyi 是全局變量,其他皆爲局部變量。

上述代碼的每一行皆爲變量賦值語句。凡用 = 賦值的對象即變量。凡用 local 修飾的變量爲局部變量,否則爲全局變量。

變量的定義,也可不賦值,例如:

foo
local bar

定義了全局變量 foo 和局部變量 bar,因爲未賦值於它們,它們的值是 nil

Lua 變量的賦值語法支持多變量賦值。例如

local w1, w2, w3 = 0.0655 * w, nil, 0.05 * w

= 右側的三個值分別賦於 = 左側的三個局部變量,結果是 w1 的值爲 0.0655 * ww2 的值爲 nilw3 的值爲 0.05 * w

倘若真的看了 card-env.tex 裏上述代碼的出處,或許會發現我撒謊了。在 card-env.tex 裏,

local w1, w2, w3 = 0.0655 * w, nil, 0.05 * w
w2 = w - (w1 + w3)

是寫成了

local w1, w2, w3 = 0.0655 * w, nil, 0.05 * w; w2 = w - (w1 + w3)

這兩種寫法等價。因爲在 Lua 語言裏,分號 ; 僅表示一條語句的終結,與換行符等效。

變量的類型只有 nil,布爾值,數字、字符串,函數,表,線程和用戶數據(userdata)。若是在 ConTeXt 源文件裏用 Lua 語言編寫程序,只需要將注意力放在前 6 種類型即可。

事實上,在上述示例裏,已經見識了類型爲 nil,數字、函數和表的變量了,亦即

mingyi = {}    -- 表

local ctx = context   -- 表
local dim = number.todimen    -- 函數
local w = tex.dimen.textwidth    -- 數字

-- 是 Lua 語言的代碼註釋符號,Lua 解釋器會忽略 -- 及其之後直到行尾的內容。

數字類型的變量,支持所有的小學數學課本里的算數。Lua 的 math 庫裏支持中學數學課本里常用的函數。大學數學裏的函數、微分、積分、級數……可能需要用 Lua 語言自行編寫。

字符串類型的變量,在 card-env.tex 裏也大量出現了,只不過它們是以函數的參數的形式出現。下面給出字符串變量賦值示例:

local a = "\\starttext ... \\stoptext"
local b = [[\starttext ... \stoptext]]
local c = [=[\starttext ... \stoptext]=]
local d = [==[\starttext ... \stoptext]==]

這 4 個字符串變量的值等價。a 的值用的是 Lua 短字符串語法,而 bcd 的值用的是 Lua 長字符串語法。

在短字符串語法裏,一些特殊字符,需要使用 \ 轉義方能將其視爲普通字符,例如 \ 本身,又例如換行符 \n——將字符 n 轉義爲換行符。

在長字符串語法裏,要表示換行,需要換行……例如:

local a = "\\starttext\n ... \n\\stoptext"

要用與上述短字符串等價的長字符串作爲 a 的值,需要寫成

local a = [[\\starttext
 ... 
\\stoptext]]

函數

在 Lua 語言裏,函數可以作爲變量的值,亦即函數並不比變量更特殊。

定義一個函數 f(x) = x,語法是

function (x)
    return x
end

寫成

function (x) return x end

亦可。

倘若像中學數學裏那樣說 y = f(x),只需寫爲

y = function (x) return x end

以上是 Lua 語言裏的匿名函數的寫法和用法。通常不需要如此行爲藝術,只需將 y 直接定義爲函數,例如

function y (x)
    return x
end

要定義多元函數,例如數學課本里的 z = f(x, y) = x + y 函數,只需

function z (y, x)
    return x + y
end

由於 Lua 語言支持多變量賦值,因此函數可以返回多個變量。例如

function f (x, y)
    return x * x, y * y, x + y
end

local a, b, c = f(2, 4)

變量 abc 的值分別爲 4,16 和 6。

表,是 Lua 語言的精華。

下面的代碼定義了一個空表:

local mingyi = {}

若令 mingyi 的索引(或下標)爲 1,2,3 的元素爲三個字符串,只需

mingyi[1] = [[\starttext]]
mingyi[2] = " ... ... ... "
mingyi[3] = [[\stoptext]]

倘若預先知道表中各個元素的值,可在定義變量時可直接將元素的值放在表裏,例如:

local mingyi = {[[\starttext]], " ... ... ... ", [[\stoptext]]

表可以是同構的,例如上述元素皆爲字符串的表,也可以是異構的,例如:

local foo = {1, "two", function (x) return x end, {3, 4, "hello"}}

函數和表也可以作爲表的元素。foo[3] 是函數,可以像下面這樣調用它:

foo[3](foo[2])

Lua 解釋器對上述語句的求值結果爲字符串 "two"

foo[4] 是表。foo[4][1] 的值爲 3。foo[4][3] 的值爲 "hello"

Lua 語言將表的索引定義爲從 1 開始,而不是 0。請記住這一點。

表的元素也可以是鍵值對。例如:

local color = {red = 0.001, green = 0.803, blue = 0.222}

有兩種訪問表 color 裏的元素的方法。例如,

color.green

color["gree"]

等價。

對於上述 color 表的定義,可繼續追加鍵值對,例如:

color.alpha = 0.5

結果,color 就有了 4 個元素。

由於 Lua 表有上述特性,因此 Lua 的程序模塊也可基於表予以構造。例如,以下代碼創建了一個叫做 mingyi 的模塊:

mingyi = {}
mingyi.foo = "Hello world!"

mingyi.y  = function (x) return x end

function mingyi.test (x, y)
    return x + y
end

return mingyi

假設上述代碼保存在 mingyi-module.lua 文件裏,那麼在與該文件位於同一目錄下的另一個 Lua 程序源文件或 ConTeXt 源文件裏……以後者爲例,在 foo.tex 文件裏載入 mingyi 模塊:

% 這是 foo.tex 文件
\environment card-env
\starttext
\startluacode
local mingyi = require("mingyi-module")
context.title(mingyi.foo)
context("1 + 2 = %d\n", mingyi.test(1, 2))
\stopluacode
\stoptext

執行 context 命令解釋 foo.tex 文件,可將其編譯爲 foo.pdf:

$ context foo

得到的 foo.pdf 如下圖所示:

Lua 的力量

雖然上例演示的 1 + 2 = 3 是非常簡單的數學運算,但是在 TeX 裏實現起來卻非常繁瑣,足以令凡人卻步:

\newcount\one
\one=1
\newcount\two
\two=2
\newcount\three
\three=0
\advance\three by \one
\advance\three by \two
1 + 2 = \the\three

因爲這個緣故,倘若不是對 TeX 有特殊的興趣,在使用 ConTeXt 做一些重要的文字排版工作時,需要數字運算,我總是建議使用 Lua 代碼。

再舉一個例子。在 card-env.tex 裏,有以下代碼:

local w = tex.dimen.textwidth
local w1, w2, w3 = 0.0655 * w, nil, 0.05 * w
w2 = w - (w1 + w3)

其中,w2 的值是基於 ww1w2 確定的。但是,在未寫這段 Lua 代碼之前,在 ConTeXt 源文件裏,與 w2 相應的排版參數,我是心算出的,即

\startxcell[width=.0655\textwidth] \stopxcell
\startxcell[width=.8845\textwidth] 成爲天才 \stopxcell
\startxcell[width=.05\textwidth]\strut\no\stopxcell

中的 .8845\textwidth,若用 TeX 來計算,大致需要像下面這樣寫,

\newdimen\first
\first=.0655\textwidth
\newdimen\third
\third=.05\textwidth
\newdimen\second
\second=\textwidth
\advance\second by -\first
\advance\second by -\third

\the\second % 該結果即 .8845\textwidth

雖然 TeX 做數學運算不那麼直觀,但是對簡單的運算尚能勝任。但是,要用它計算 (1 - 0.655 - 0.05) 這樣的數學題,它就覺得好難了。

條件

若一段程序需要符合某個條件方能執行,需要使用以下語法:

if 條件成立 then
    一段程序
end

例如,

local ctx = context
local dim = number.todimen
local w = tex.dimen.textwidth
local w1, w2, w3 = 0.0655 * w, nil, 0.05 * w
w2 = w - (w1 + w3)

function mingyi.make_row(row, n)
    ctx.startxrow()
    ctx.startxcell{width = dim(w1)}
    context([[\m{\cdot}]])
    ctx.stopxcell()
    if n == 1 then
        ctx.startxcell{width = dim(w2)}
        context(row[1])
        ctx.stopxcell()
        ctx.startxcell{width = dim(w3)}
        context([[\hfill\strut ]] .. row[2])
        ctx.stopxcell()
    else
        ctx.startxcell()
        context(row[1])
        ctx.stopxcell()
        ctx.startxcell()
        context([[\hfill\strut ]] .. row[2])
        ctx.stopxcell()        
    end
    ctx.stopxrow()
end

如果條件不成立也要執行一段程序,可使用以下語法

if 條件成立 then
    一段程序
else
    另一段程序
end

如果要根據多個條件選擇某段程序予以執行,可使用以下語法

if 條件 1 成立 then
    程序段 1
elseif 條件 2 成立 then
    程序段 2
else if ... then
    ... ... ...
else
    以上條件都不成立時會被執行的程序段
end

沒必要再舉例了。

迭代

在 ConTeXt 源文件裏,Lua 代碼的最大的用武之地有二。一是數值運算,二是迭代(亦稱循環)。

迭代主要用於做兩類事情。一類是需要對一段程序反覆執行有限次。另一類是,遍歷表裏的每個元素。

第一類事情,本質上是對整數集的遍歷,例如計算 1 + 2 + ... + 10,

local sum = 0
for i = 1, 10 do
    sum = sum + i
end

上述迭代過程,i 的步進值爲 1,即每次迭代過程結束後,i 的值增 1。

再例如,計算 1 + 3 + 5 + 7 + 9,可將上述迭代過程的變量 i 的步進值變爲 2,即

local sum = 0
for i = 1, 10, 2 do
    sum = sum + i
end

第二類迭代過程應用最爲廣泛,無論是遍歷字符串,還是遍歷表,皆依賴它。有連個函數,pairsipairs 可在這種迭代過程中使用。

pairs 可用於遍歷由鍵值對構成的表。例如

\environment card-env
\starttext
\startluacode
local color = {red = 0.001, green = 0.122, blue = 1.000}
context.startitemize({"inmargin", "broad"})
for k, v in pairs(color) do
    context.item(string.format("%s\t= %f", k, v))
end
context.stopitemize()
\stopluacode
\stoptext

輸出結果爲:

由鍵值對構成的表,遍歷它時,鍵值對的順序未必是在表在構造時鍵值對的出現順序。

ipairs 可用於在迭代過程裏遍歷索引表,即元素索引爲 1, 2, ... 的表。如果希望表中元素依序遍歷,應該考慮用索引表而非鍵值對錶。ipairs 的用法與 pairs 相似,例如,

\environment card-env
\starttext
\startluacode
local function vector(v, s)
    local n = #v  -- 索引表 v 裏元素的個數
    context("\\[")
    for i, e in ipairs(v) do
        if i == n then
            context(s, e)
        else
            context(s .. ", ", e)
        end
    end
    context("\\]")
end

x = {0.1, 0.2, 0.3}
y = {0.4, 0.6, 0.1}
z = {}
for i, v in ipairs(x) do
    z[i] = x[i] + y[i]
end

local s = "%.1f"
context.startformula()
vector(x, s); context(" + "); vector(y, s); context(" = "); vector(z, s)
context.stopformula()
\stopluacode
\stoptext

輸出結果爲

如果在遍歷表的過程中,不需要獲得鍵或索引,可使用 _ 變量,避免爲它們費心取一個名字,例如,

for _, v in pairs(foo) do
    ... ... ...
end

結語

關於 Lua,目前我幾乎就懂這麼多。在 ConTeXt 裏使用 Lua,我樂觀地覺得,我所掌握的這點 Lua 知識應該是夠用的。

Lua 是一門小而優美的編程語言,即使不在 ConTeXt 裏使用,也值得拿出一些時間學習如何使用它編程。倘有此心,我推薦《Programming in Lua》這本書。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章