lua

2021-12-03 · 3 min read

Syntax #

function foo(x)
  -- <body>
end

-- local to the scope
local function bar()
  -- <body>
end

-- anonymous function (all fns are technically anonymous)
local max = function (x, y)
  return (a > b) and a or b
end

-- simulate "named" args
function rename(args) do
  os.rename(args.old, args.new)
end
-- can call w/o parens
rename{ new = "perm.lua", old = "temp.lua" }
if x then
  -- <case 1>
elseif y then
  -- <case 2>
else
  -- <case 3>
end
while x do
  -- <block>
end
repeat
  -- <block>
until x
-- note end is _inclusive_
for i = 1, 3 do
  print(i)
end

-- output:
-- > 1
-- > 2
-- > 3
for i = 3, 1, -1 do
  print(i)
end

-- output:
-- > 3
-- > 2
-- > 1
-- iterate over an _array_
local a = { 1, 3, 5 }
for i, x in ipairs(a) do
  print(i, x)
end

-- output:
-- > 1	1
-- > 2	3
-- > 3	5
-- iterate over a table (order not guaranteed)
local a = { 1, 3, 5 }
for i, x in ipairs(a) do
  print(i, x)
end

-- output:
-- > 1	10
-- > 2	foo
-- > x	5
-- > y	3
-- creates a new scope
do
  -- <block>
end
-- multiple assignment
a, b = 10, 20
-- this is a block comment
--[[
print(x)
--]]
-- new table
x = {}
x["foo"] = 5
-- array (note 1-indexed)
local a = {}
for i = 1, 5 do
  a[i] = math.random(10)
end

Semantics #

  • undefined variables default to nil
  • _ALL_CAPS variables are reserved for lua
  • types: nil, boolean, number (double), string (immutable, just dumb byte vec), table (map type), function, userdata (opaque blobs from external libs), thread (but actually coroutine, not OS thread)
  • all values are true except false and nil
  • arrays are just tables with number keys
  • variables are global by default, need to declare with local to restrict to lexical scoping
  • arrays cannot have holes, but can be partially filled until used

Idioms #

-- default value
x = x or "default"

Strings #

-- string concat
"foo" .. " bar"
-- string length
#"foo"
-- multiline strings
escapes = [[
  \f (form feed)
  \n (newline)
  \r (carriage return)
]]
print("|" .. escapes .. "|")

-- output:
-- > |  \f (form feed)
-- >   \n (newline)
-- >   \r (carriage return)
-- > |

Arrays #

-- remove last entry
a[#a] = nil
-- or
table.remove(a, #a)

-- append entry
a[#a + 1] = "foo"
-- or
table.insert(a, #a + 1, "foo")

-- insert (shifting other elts)
table.insert(a, 1, "foo")

-- sort
table.sort({ 5, 3, 9, 1, 7})
-- > { 1, 3, 5, 7, 9 }

-- concat
table.concat({ 1, 2, 3 }, " ")
-- > "1 2 3"

Structs #

point = { x = 10, y = 20 }
print(point.x)

Iterators #

  • an iterator is just a function with some local state. each time you call the function it returns the next value and nil when finished.

example:

-- returning a closure that modifies the outer fn args each call
function fromto(a, b)
  return function ()
    if a > b then
	  return nil
	else
	  a = a + 1
	  return a - 1
    end
  end
end

for x in fromto(2, 5) do
  print(x)
end

-- > 2
-- > 3
-- > 4
-- > 5
  • there are also stateless iterators
function fromto(a, b)
  return function (state, seed)
    if seed >= state then
	  return nil
	else
	  return seed + 1
	end
  end, b, a - 1 -- notice the state, seed returned here
end
  • and "seedless" iterators
function fromto(a, b)
  return function(state)
    if state[1] > state[2] then
	  return nil
	else
	  state[1] = state[1] + 1
	  return state[1] - 1
	end
  end, { a, b } -- this is the initial state
end

Errors #

expressing errors:

  1. can return out, err, where out is nil if there was an error and err is an error message
  2. can raise an exception with e.g. assert(cond) or error("error message")
    • error has an optional 2nd arg that specifies the "error-level", where error("..", 1) means "this function is to blame", error("..", 2) means "the calling function is to blame", error("..", 3) means the caller's caller, etc...

catching errors:

pcall(function, arg1, arg2, ..) will return ok, result where ok: boolean and result is the succesful output or the error message.

also

xpcall(
	function ()
	  -- do fallible thing
	end,
	function (err)
	  -- handle error
	end
)

debug.traceback() returns the traceback as a string

Modules #

  • modules are just tables
  • some standard modules: table, io, string, math, os, coroutine, package, debug, all included in the prelude
  • "import" a module with mymodule = require "mymodule"
  • require searches package.path to find modules
    • can (usually) modify the path with LUA_PATH env var
    • lua replaces ";;" in the env var with the pre-compiled default
    • the format is a series of ";" separated template strings, where "?" is replaced with the module name
    • e.g., "/usr/local/share/lua/5.2/?.lua;/usr/local/share/lua/5.2/?/init.lua;./?.lua"
    • lua replaces "." in the module name with platform path separator. used to import from packages with many modules

defining a module

-- file: complex.lua
local complex = {}

function complex.new(r, i)
  return { real = r or 0, im = i or 0 }
end

complex.i = complex.new(0, 1)

function complex.add(c1, c2)
  return complex.new(c1.real + c2.real, c1.im + c2.im)
end

function complex.tostring(c)
  return tostring(c.real) .. "+" .. tostring(c.im) .. "i"
end

return complex

Metatables #

  • can set metatable for a table to override built-in behaviours, e.g., arithmetic, tostring, equality, iteration, call table as fn, etc...
  • use setmetatable and getmetatable functions
  • metamethods: __add, __sub, __mul, __div, __mod, __pow, __unm, __concat, __len, __eq, __lt, __le, __index, __newindex, __call, __tostring, __ipairs, __pairs, __gc
  • __index and __newindex can be tables, used for single-inheritance OO

augmenting complex.lua defined above with metatables

local complex = {}
local mt = {}

function complex.new(r, i)
  return setmetatable({ real = r or 0, im = i or 0 }, mt)
end

function complex.is_complex(c)
  return getmetatable(c) == mt
end

complex.i = complex.new(0, 1)

function complex.add(c1, c2)
  return complex.new(c1.real + c2.real, c1.im + c2.im)
end
mt.__add = complex.add

function complex.eq(c1, c2)
  return (c1.real == c2.real) and (c1.im == c2.im)
end
mt.__eq = complex.eq

function complex.norm(c)
  return math.sqrt(c.real * c.real + c.im * c.im)
end
mt.__len = complex.norm

function complex.tostring(c)
  return tostring(c.real) .. "+" .. tostring(c.im) .. "i"
end
mt.__tostring = complex.tostring

return complex

OO #

  • using dot-notation doesn't add implicit self to call. you need to include it manually like obj.method(obj, args)
  • to avoid this duplication, lua has colon-notation which adds this implicit self as the first arg: obj:method(args).
  • declaring functions using a obj:method adds implicit self parameter
function point:norm()
  return math.sqrt(self.x * self.x + self.y * self.y)
end

can define prototype-style classes

local Complex = {}
Complex.__index = Complex

function Complex:new(r, i)
  return setmetatable({ real = r or 0, im = i or 0 }, self)
end

function Complex:norm()
  return math.sqrt(self.real * self.real + self.im * self.im)
end

-- etc...

return Complex

adding other fields to Complex makes them default values for new instances

local Complex = { real = 0, im = 0 }

single inheritance (Point extends Shape)

local Shape = {}
Shape.__index = Shape

function Shape:new(x, y)
  return setmetatable({ x = x, y = y }, self)
end

function Shape:move(dx, dy)
  self.x = self.x + dx
  self.y = self.y + dy
end

return Shape
local Shape = require "shape"
local Point = setmetatable({}, Shape)
Point.__index = Point

function Point:area()
  return 0
end

return Point
local Point = require "point"
p = Point:new(5, 10)
print(p:area())
-- > 0
p:move(-3, 7)
print(p.x, p.y)
-- > 2    17

this is a simple version of OO in lua (there are more sophisticated versions). some disadvantages of this approach: 1. class methods (e.g. new) visible in instance namespace, 2. metamethods are not automatically inherited (need to be explicitly set)