lua

2021-12-03 ยท 7 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)