1. 又要让我学习新知识?

本文是我学习Lua语言的一个开端。至于又要学习一门新编程语言的动机,在《进化 neovim 到 lin.nvim 风味》中已经自述的很明白了:成年人的世界没有简单二字,要是有得选,谁愿意把自己逼成文武全才~ 囧rz

其实我心里特反感没完没了的学习。在我看来,对于一个已经不是学生身份的人来说,让自己沉溺于所谓的“学习”,是在用”战术的勤奋”掩盖”战略的懒惰”。 成年人的学习必须有输出结果,不然的话就不要学什么习了

正是基于此理念:(1)我通过研读本文初步掌握Lua语言之后,会应用Lua语言复刻《Steam Deck 掌机催我搞技术新基建》中shell脚本工具(附在文末)。(2)在学习的过程中,我会在下文的2. 速战速决Lua脚本语言中给出要点解读;同时为了验证我的理解,我会用Mathematica语言对该Lua程序重写。

这么大的工作量没有神灵庇护怎么行!必须得把我的神仙请出来,保佑我能废寝忘食地专心学习😍:

我的 Learn Lua in Y minutes-Blue and purple light girl.jpg

2. 速战速决Lua脚本语言

2.1. 原文:Learn Lua in Y minutes

原文链接是Learn X in Y minutes, Where X=Lua. 这篇短小的教程是Literate Programming的方式写成,全文如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385

-- https://learnxinyminutes.com/docs/lua/
--
-- Two dashes start a one-line comment.

--[[
Adding two ['s and ]'s makes it a
multi-line comment.
--]]

----------------------------------------------------
-- 1. Variables and flow control.
----------------------------------------------------

num = 42 -- All numbers are doubles.
-- Don't freak out, 64-bit doubles have 52 bits for
-- storing exact int values; machine precision is
-- not a problem for ints that need < 52 bits.

s = 'walternate' -- Immutable strings like Python.
t = "double-quotes are also fine"
u = [[ Double brackets
start and end
multi-line strings.]]
t = nil -- Undefines t; Lua has garbage collection.

-- Blocks are denoted with keywords like do/end:
while num < 50 do
num = num + 1 -- No ++ or += type operators.
end

-- If clauses:
if num > 40 then
print('over 40')
elseif s ~= 'walternate' then -- ~= is not equals.
-- Equality check is == like Python; ok for strs.
io.write('not over 40\n') -- Defaults to stdout.
else
-- Variables are global by default.
thisIsGlobal = 5 -- Camel case is common.

-- How to make a variable local:
local line = io.read() -- Reads next stdin line.

-- String concatenation uses the .. operator:
print('Winter is coming, ' .. line)
end

-- Undefined variables return nil.
-- This is not an error:
foo = anUnknownVariable -- Now foo = nil.

aBoolValue = false

-- Only nil and false are falsy; 0 and '' are true!
if not aBoolValue then print('it was false') end

-- 'or' and 'and' are short-circuited.
-- This is similar to the a?b:c operator in C/js:
ans = aBoolValue and 'yes' or 'no' --> 'no'

karlSum = 0
for i = 1, 100 do -- The range includes both ends.
karlSum = karlSum + i
end

-- Use "100, 1, -1" as the range to count down:
fredSum = 0
for j = 100, 1, -1 do fredSum = fredSum + j end

-- In general, the range is begin, end[, step].

-- Another loop construct:
repeat
print('the way of the future')
num = num - 1
until num == 0


----------------------------------------------------
-- 2. Functions.
----------------------------------------------------

function fib(n)
if n < 2 then return 1 end
return fib(n - 2) + fib(n - 1)
end

-- Closures and anonymous functions are ok:
function adder(x)
-- The returned function is created when adder is
-- called, and remembers the value of x:
return function (y) return x + y end
end
a1 = adder(9)
a2 = adder(36)
print(a1(16)) --> 25
print(a2(64)) --> 100

-- Returns, func calls, and assignments all work
-- with lists that may be mismatched in length.
-- Unmatched receivers are nil;
-- unmatched senders are discarded.

x, y, z = 1, 2, 3, 4
-- Now x = 1, y = 2, z = 3, and 4 is thrown away.

function bar(a, b, c)
print(a, b, c)
return 4, 8, 15, 16, 23, 42
end

x, y = bar('zaphod') --> prints "zaphod nil nil"
-- Now x = 4, y = 8, values 15...42 are discarded.

-- Functions are first-class, may be local/global.
-- These are the same:
function f(x) return x * x end
f = function (x) return x * x end

-- And so are these:
local function g(x) return math.sin(x) end
local g; g = function (x) return math.sin(x) end
-- the 'local g' decl makes g-self-references ok.

-- Trig funcs work in radians, by the way.

-- Calls with one string param don't need parens:
print 'hello' -- Works fine.


----------------------------------------------------
-- 3. Tables.
----------------------------------------------------

-- Tables = Lua's only compound data structure;
-- they are associative arrays.
-- Similar to php arrays or js objects, they are
-- hash-lookup dicts that can also be used as lists.

-- Using tables as dictionaries / maps:

-- Dict literals have string keys by default:
t = {key1 = 'value1', key2 = false}

-- String keys can use js-like dot notation:
print(t.key1) -- Prints 'value1'.
t.newKey = {} -- Adds a new key/value pair.
t.key2 = nil -- Removes key2 from the table.

-- Literal notation for any (non-nil) value as key:
u = {['@!#'] = 'qbert', [{}] = 1729, [6.28] = 'tau'}
print(u[6.28]) -- prints "tau"

-- Key matching is basically by value for numbers
-- and strings, but by identity for tables.
a = u['@!#'] -- Now a = 'qbert'.
b = u[{}] -- We might expect 1729, but it's nil:
-- b = nil since the lookup fails. It fails
-- because the key we used is not the same object
-- as the one used to store the original value. So
-- strings & numbers are more portable keys.

-- A one-table-param function call needs no parens:
function h(x) print(x.key1) end
h{key1 = 'Sonmi~451'} -- Prints 'Sonmi~451'.

for key, val in pairs(u) do -- Table iteration.
print(key, val)
end

-- _G is a special table of all globals.
print(_G['_G'] == _G) -- Prints 'true'.

-- Using tables as lists / arrays:

-- List literals implicitly set up int keys:
v = {'value1', 'value2', 1.21, 'gigawatts'}
for i = 1, #v do -- #v is the size of v for lists.
print(v[i]) -- Indices start at 1 !! SO CRAZY!
end
-- A 'list' is not a real type. v is just a table
-- with consecutive integer keys, treated as a list.

----------------------------------------------------
-- 3.1 Metatables and metamethods.
----------------------------------------------------

-- A table can have a metatable that gives the table
-- operator-overloadish behavior. Later we'll see
-- how metatables support js-prototype behavior.

f1 = {a = 1, b = 2} -- Represents the fraction a/b.
f2 = {a = 2, b = 3}

-- This would fail:
-- s = f1 + f2

metafraction = {}
function metafraction.__add(f1, f2)
sum = {}
sum.b = f1.b * f2.b
sum.a = f1.a * f2.b + f2.a * f1.b
return sum
end

setmetatable(f1, metafraction)
setmetatable(f2, metafraction)

s = f1 + f2 -- call __add(f1, f2) on f1's metatable

-- f1, f2 have no key for their metatable, unlike
-- prototypes in js, so you must retrieve it as in
-- getmetatable(f1). The metatable is a normal table
-- with keys that Lua knows about, like __add.

-- But the next line fails since s has no metatable:
-- t = s + s
-- Class-like patterns given below would fix this.

-- An __index on a metatable overloads dot lookups:
defaultFavs = {animal = 'gru', food = 'donuts'}
myFavs = {food = 'pizza'}
setmetatable(myFavs, {__index = defaultFavs})
eatenBy = myFavs.animal -- works! thanks, metatable

-- Direct table lookups that fail will retry using
-- the metatable's __index value, and this recurses.

-- An __index value can also be a function(tbl, key)
-- for more customized lookups.

-- Values of __index,add, .. are called metamethods.
-- Full list. Here a is a table with the metamethod.

-- __add(a, b) for a + b
-- __sub(a, b) for a - b
-- __mul(a, b) for a * b
-- __div(a, b) for a / b
-- __mod(a, b) for a % b
-- __pow(a, b) for a ^ b
-- __unm(a) for -a
-- __concat(a, b) for a .. b
-- __len(a) for #a
-- __eq(a, b) for a == b
-- __lt(a, b) for a < b
-- __le(a, b) for a <= b
-- __index(a, b) <fn or a table> for a.b
-- __newindex(a, b, c) for a.b = c
-- __call(a, ...) for a(...)

----------------------------------------------------
-- 3.2 Class-like tables and inheritance.
----------------------------------------------------

-- Classes aren't built in; there are different ways
-- to make them using tables and metatables.

-- Explanation for this example is below it.

Dog = {} -- 1.

function Dog:new() -- 2.
newObj = {sound = 'woof'} -- 3.
self.__index = self -- 4.
return setmetatable(newObj, self) -- 5.
end

function Dog:makeSound() -- 6.
print('I say ' .. self.sound)
end

mrDog = Dog:new() -- 7.
mrDog:makeSound() -- 'I say woof' -- 8.

-- 1. Dog acts like a class; it's really a table.
-- 2. function tablename:fn(...) is the same as
-- function tablename.fn(self, ...)
-- The : just adds a first arg called self.
-- Read 7 & 8 below for how self gets its value.
-- 3. newObj will be an instance of class Dog.
-- 4. self = the class being instantiated. Often
-- self = Dog, but inheritance can change it.
-- newObj gets self's functions when we set both
-- newObj's metatable and self's __index to self.
-- 5. Reminder: setmetatable returns its first arg.
-- 6. The : works as in 2, but this time we expect
-- self to be an instance instead of a class.
-- 7. Same as Dog.new(Dog), so self = Dog in new().
-- 8. Same as mrDog.makeSound(mrDog); self = mrDog.

----------------------------------------------------

-- Inheritance example:

LoudDog = Dog:new() -- 1.

function LoudDog:makeSound()
s = self.sound .. ' ' -- 2.
print(s .. s .. s)
end

seymour = LoudDog:new() -- 3.
seymour:makeSound() -- 'woof woof woof' -- 4.

-- 1. LoudDog gets Dog's methods and variables.
-- 2. self has a 'sound' key from new(), see 3.
-- 3. Same as LoudDog.new(LoudDog), and converted to
-- Dog.new(LoudDog) as LoudDog has no 'new' key,
-- but does have __index = Dog on its metatable.
-- Result: seymour's metatable is LoudDog, and
-- LoudDog.__index = LoudDog. So seymour.key will
-- = seymour.key, LoudDog.key, Dog.key, whichever
-- table is the first with the given key.
-- 4. The 'makeSound' key is found in LoudDog; this
-- is the same as LoudDog.makeSound(seymour).

-- If needed, a subclass's new() is like the base's:
function LoudDog:new()
newObj = {}
-- set up newObj
self.__index = self
return setmetatable(newObj, self)
end

----------------------------------------------------
-- 4. Modules.
----------------------------------------------------


--[[ I'm commenting out this section so the rest of
-- this script remains runnable.

-- Suppose the file mod.lua looks like this:
local M = {}

local function sayMyName()
print('Hrunkner')
end

function M.sayHello()
print('Why hello there')
sayMyName()
end

return M

-- Another file can use mod.lua's functionality:
local mod = require('mod') -- Run the file mod.lua.

-- require is the standard way to include modules.
-- require acts like: (if not cached; see below)
local mod = (function ()
<contents of mod.lua>
end)()
-- It's like mod.lua is a function body, so that
-- locals inside mod.lua are invisible outside it.

-- This works because mod here = M in mod.lua:
mod.sayHello() -- Prints: Why hello there Hrunkner

-- This is wrong; sayMyName only exists in mod.lua:
mod.sayMyName() -- error

-- require's return values are cached so a file is
-- run at most once, even when require'd many times.

-- Suppose mod2.lua contains "print('Hi!')".
local a = require('mod2') -- Prints Hi!
local b = require('mod2') -- Doesn't print; a=b.

-- dofile is like require without caching:
dofile('mod2.lua') --> Hi!
dofile('mod2.lua') --> Hi! (runs it again)

-- loadfile loads a lua file but doesn't run it yet.
f = loadfile('mod2.lua') -- Call f() to run it.

-- load is loadfile for strings.
-- (loadstring is deprecated, use load instead)
g = load('print(343)') -- Returns a function.
g() -- Prints out 343; nothing printed before now.

--]]

2.2. Mathematica复刻Lua代码

在这一部分,我将用Mathematica语言对上述的Lua代码重写;并将Lua代码的原文(包括行号)以Mathematica注释的形式给出。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
(* 
行号 Lua代码原文
15 num = 42 -- All numbers are doubles.
16 -- Don't freak out, 64-bit doubles have 52 bits for
17 -- storing exact int values; machine precision is
18 -- not a problem for ints that need < 52 bits.
19
20 s = 'walternate' -- Immutable strings like Python.
21 t = "double-quotes are also fine"
22 u = [[ Double brackets
23 start and end
24 multi-line strings.]]
25 t = nil -- Undefines t; Lua has garbage collection.
26
*)

num = 42

s = "walternate"
t = "double-quotes are also fine"
u = "Double brackets
start and end
multi-line strings."

t = Null

(*
行号 Lua代码原文
28 while num < 50 do
29 num = num + 1 -- No ++ or += type operators.
30 end
*)
While[num < 50, num = num + 1]

(*
行号 Lua代码原文
32 -- If clauses:
33 if num > 40 then
34 print('over 40')
35 elseif s ~= 'walternate' then -- ~= is not equals.
36 -- Equality check is == like Python; ok for strs.
37 io.write('not over 40\n') -- Defaults to stdout.
38 else
39 -- Variables are global by default.
40 thisIsGlobal = 5 -- Camel case is common.
41
42 -- How to make a variable local:
43 local line = io.read() -- Reads next stdin line.
44
45 -- String concatenation uses the .. operator:
46 print('Winter is coming, ' .. line)
47 end
*)

If[num > 40,
Print["over 40"],
If[s != "walternate",
Print["not over 40\n"],
thisIsGlobal = 5;
(* Wolfram Mathematica的局部变量是通过 `Module`,`With`等函数实现 *)
]]

(*
行号 Lua代码原文
62 karlSum = 0
63 for i = 1, 100 do -- The range includes both ends.
64 karlSum = karlSum + i
65 end
66
67 -- Use "100, 1, -1" as the range to count down:
68 fredSum = 0
69 for j = 100, 1, -1 do fredSum = fredSum + j end
70
71 -- In general, the range is begin, end[, step].
72
73 -- Another loop construct:
74 repeat
75 print('the way of the future')
76 num = num - 1
77 until num == 0
*)

karlSum = 0;
Do[karlSum = karlSum + 1, {i, 1, 100}]

fredSum = 0;
Do[fredSum = fredSum + j, {j, 100, 1, -1}]

num = 10;
Until[num == 0, Print["the way of the future"]; num = num - 1]

(*
行号 Lua代码原文
84 function fib(n)
85 if n < 2 then return 1 end
86 return fib(n - 2) + fib(n - 1)
87 end
88
89 -- Closures and anonymous functions are ok:
90 function adder(x)
91 -- The returned function is created when adder is
92 -- called, and remembers the value of x:
93 return function (y) return x + y end
94 end
95 a1 = adder(9)
96 a2 = adder(36)
97 print(a1(16)) --> 25
98 print(a2(64)) --> 100
*)

fib[n_] := Module[{}, If[n < 2, 1, fib[n - 2] + fib[n - 1]]]

addr[x_, y_] := Module[{}, x + y]
a1 = CurryApplied[addr, 2][9];
a2 = CurryApplied[addr, 2][36];
Print[a1[16]]
Print[a2[64]]

(*
行号 Lua代码原文
143 -- Dict literals have string keys by default:
144 t = {key1 = 'value1', key2 = false}
145
146 -- String keys can use js-like dot notation:
147 print(t.key1) -- Prints 'value1'.
148 t.newKey = {} -- Adds a new key/value pair.
149 t.key2 = nil -- Removes key2 from the table.
150
151 -- Literal notation for any (non-nil) value as key:
152 u = {['@!#'] = 'qbert', [{}] = 1729, [6.28] = 'tau'}
153 print(u[6.28]) -- prints "tau"
154
155 -- Key matching is basically by value for numbers
156 -- and strings, but by identity for tables.
157 a = u['@!#'] -- Now a = 'qbert'.
158 b = u[{}] -- We might expect 1729, but it's nil:
*)

t = <|key1 -> "value1", key2 -> False|>;
Print[t[key1]]
t[newKey] = Null
t[key2] = Null;

u = <|"@!#" -> "qbert", Null -> 1729, 6.28 -> "tau"|>;
Print[u[6.28]]

a = u["@!#"] (* a = "qbert" *)
b = u[Null] (* b = 1729 *)

(*
行号 Lua代码原文
168 for key, val in pairs(u) do -- Table iteration.
169 print(key, val)
170 end
*)

KeyValueMap[{#1, #2} &, u]

2.3. Learn … minutes要点解读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
100 -- Returns, func calls, and assignments all work
101 -- with lists that may be mismatched in length.
102 -- Unmatched receivers are nil;
103 -- unmatched senders are discarded.
104
105 x, y, z = 1, 2, 3, 4
106 -- Now x = 1, y = 2, z = 3, and 4 is thrown away.
107
108 function bar(a, b, c)
109 print(a, b, c)
110 return 4, 8, 15, 16, 23, 42
111 end
112
113 x, y = bar('zaphod') --> prints "zaphod nil nil"
114 -- Now x = 4, y = 8, values 15...42 are discarded.

从上述代码可知:Lua的赋值语句是“多退少补”。

待实习的要点:

  • 元表(metatable)类似于数据模板;元方法(metamethod)则是与某个元表绑定的函数。绑定的方法是setmetatable(sometable, metatable). 钻研材料Lua 元表(Metatable)
  • 面向对象。
  • 模块(Module)的搜索路径。
  • 模块(Module)的编写方法。

3. Lua复刻Shell脚本工具

Shell脚本所在位置:Steam Deck 掌机催我搞技术新基建。源代码如下:

1
2
3
4
5
6
7
8
9
#! /bin/zsh

echo "Input parent directory of shared forlds, without tailing '/':"
read parentDirectory

sharedFolders=($(ls $parentDirectory))

for itr in $sharedFolders
ln -s "$parentDirectory/$itr" "$HOME/$itr"

用本文所学习的lua语言对上述脚本代码重写,得到:

1
2
3
4
5
6
7
print("Input parent directory of shared folders, with tailing '/':")
parentDirectory = io.read()

for itr in io.popen("ls " .. parentDirectory):lines() do
io.popen("ln -s " .. parentDirectory .. itr .. " $HOME/" .. itr)
end

上述shell脚本和lua脚本二者实现相同的功能。但是,shell的不同软件(譬如bashzsh)的shell语法是有区别的;另一方面,lua脚本用到的系统指令在不同Linux和shell终端是通用的,所以lua脚本工具具有更好的通用性。