《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》这本书。

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