Lua for the Python programmer

1. Introduction

This is primarily for myself to finally get over the hesitation about Lua and learn useful things about it. One way to do that is to compare and contrast with another languages in the same 'space' and one I'm more familiar with -- Python.

Python and Lua:

NOTE: I'm not going to cover "why not" aspects. If you don't want to use Lua, you will find many reasons, I'm sure ;)

2. Hello World

{hello.lua 2}
-- Single line comments in Lua start with two dashes: --
-- what I like about the dashes is that I don't have to use shift key
-- as I have to in Python to type the # character.

-- the print statement is familiar to Python(3) programmers
print("Hello, World!")

-- you can also use the single quote character for strings
-- which I prefer anyway
print('Hello, again!')

-- multi line strings are bookended by two square brackets
message = [[Dear Python Programmer,
Lua is a small, fast, and handy language
that is often embedded in larger programs, like
video games ...]]

{datatypes.lua 2}
-- All n umbers are internally represented as 64 bit double
-- of which 52 bits are for numeric representation

x = 42 -- this declares a global variable x
print(x)

-- you can modify it
x = 43
print(x)


-- Lua has nil for Python's None

n = nil



-- what if you want to print
-- x is 43
-- this would fail:
--    print("x is " + x)
print('x is :' .. x)

-- string concatenation in lua is two dots: ..

print('my name is Lua and I mm' .. 2020-1990 .. ' years old')

-- the above number is coerced

print(tonumber('10')) -- tonumber will convert '10' to a number

print(#"hello") -- # will give you length of the string

-- we will come to string formatting later

@ Interpreter

se the -e command line flag to evaluate code in the command line:

% lua -e "print(math.sin(12)) "
-0.536...

The -l option loads a library

% lua -i -l a -e "x = 10"

The above will enter the interactive mode afer loading the library a and setting the value of x to 10.

The default prompt of Lua is >. To change it, set the variable _PROMPT:

% lua -i -e "_PROMPT= lua> "

This will result in a lua> prompt.

You can preload a lua file BEFORE lua runs its arguments by setting the LUA_INIT environmental variable.

In a command like: % lua -e "sin=math.sin" lua script a b, the following args are set in the arg table:

{chap1_args.txt 2}
    > for i, v in pairs(arg) do print(i,v) end
    1       a
    2       b
    0       chap1a.lua
    -4      lua
    -3      -i
    -2      -e
    -1      sin=math.sin

Lua is a dynamically typed language like Python. Much like Python, you can query the type of a variable at run time usig the type() function. Example: type("hello") will print string.

type("hello") -- string
type(124) -- number
type(nil) -- nil
type(true) -- boolean
type(12.43) -- number
type(type) -- function
type({}) -- table

There are eight basic types in Lua: nil, boolean, number, string, userdata, function, thread and table.

Unlike Python, the boolean values true and false are not capitalized.

You can assign functions to variables like in Python, because functions are first class values:

a = print a('hello') hello

userdata type allows arbitrary C data to be stored in Lua variables.

3. Expressions

Expressions denote values. Types of expressions:

Arithmetic operators:

+ - * / ^ % -

Relational operators:

< > <= >= == ~=

~= is the same as != in Python.

Logical operators, much like Python:

and or not

Operator Precedence, from highest to lowest

^
not     #   - (unary)
*   /   %
+   -
..
<   >   <=  >=  ~=  ==
and
or

binary operators are left associative, ^ and .. are right associative.

4. Statements

Statement types:

Multiple assignment:

a, b = 10, 2*10
x,y = y,x -- swaps x and y
a,b,c = 10, 20 -- c is nil

Local variable's scope is limited to the block where they are declared. Note, in the interactive mode, each line is a chunk by itself and hence the scoping is different. Use the do .. end block to create scope.

a = 100
do
    local a = 10
    a = a + 1
    print(a) -- 11
end
print(a) -- 100

Its a good idea to use local whenever possible. This is one detail where Python and lua are opposite. In Python, a global variable has to be explicitely declard, local is the default, where as in Lua, global is the default and local has to be explicit. I like Python's default.

foo = 100
do
    local foo = foo
    foo = foo + 1
    print(foo) -- 101
end
print(foo) -- 100

5. Control structures

{control.lua 5}
num = 42

-- lua does not use block syntax like Python
-- you have "then" instead of ":" and the block
-- ends with a "end" keyword

if num > 40 then
	print(num)
end

num = 0
-- lua has elseif vs python's elif
if num == 0 then
	print("num is zero")
elseif num < 0 then
	print("num is negative")
else
	print("number is positive")
end

-- what's equivalent of python's range?
-- Python:
-- for i in range(10):
--     print(i)
for i = 1,10 do
	print(i)
end

-- the same can be written in a single line
-- since we do not have to follow block syntax

for i = 10, 1, -1 do print(i) end

-- TODO: for comprehension

-- lua also has repeat loop construct
i = 12
repeat
	print(i)
	i = i - 1
until i < 10

-- There is no equivalent to Python's -= or +=

6. Functions

Functions are defined with function keyword in Lua vs python's def

{functions.lua 6}
function add(x, y)
	return x + y
end

print(add(3, 4))

A function has three thigns:

  1. name
  2. parameters list
  3. body

parameters act as local variables.

If the number of parameters provided to a function call are different than the formal parameters of the function, lua "adjusts" the parameters

function x(a,b) return a + b end
x(9) -- results in error. b is nil
x(9,9) -- 18
x(9,8,7) -- 17. 7 is ignored

You can return multiple values from a function like this:

function swap(x, y)
    return y, x
end
a = 1; b = 2
a, b = swap(a,b)
print(a,b) -- 2 1

Again the extra values are discarded:

a = swap(a,b)
print(a) -- 2

Use the table.unpack function to convert a returned array with index starting from 1. unpack allows you to call any function, with any arguments, dynamically. This is how the generic call mechanism is implemented.

a, b = string.find("helper", "lp")
print(a, b) -- 3 4
string.find(table.unpack({"helper", "lp"}))

Note: before lua 5.3?, unpack was a global function. now its in the table module.

Use three dots ... to define a function with variable number of arguments:

function add (...)
    local s = 0
    for i, v in ipairs{...} do
        s = s + v
    end
    return s
end
print(add(1,2,3)) -- 6

One cool side effect of this is the multi-value id function:

function id (...) return ... end

This is a way to implement a **kwargs like functions in Python.

function foo(x, ...)
    print(x)
    -- do something with the rest ...
    -- x is the fixed parameter
end

Use the select function "selector" to pick a parameter at a position. select(3, ...) will return the param at the 3rd position.

Lua DOES NOT support named arguments.

-- invalid code
function foo(x="hello", y="world")
-- alternatively..
function foo(arg)
    return arg.x .. ',' .. arg.y
end
print(foo{x='hello', y='world'}) -- hello, world

Lua has a special syntax for "object oriented calls" -- the colon operator :.

obj:foo(x) and obj.foo(obj, x) are the same. The : saves you from typing the obj as the first parameter to the function foo. In Python, a method called on object obj automatically passes the self as the first parameter. This is where the notations are different between Python and Lua.

You can alternatively declare a function like this: foo = function (x) return x + 3 end because functions are anonymous.

When a function is written inside another function, it has full access to the local variables of the enclosing funciton; this feature is called lexical scoping.

function sortbygrade(name, grades)
    table.sort(names, function (n1, n2)
        return grades[n1] > grades[n2]
        end)

In the encosing function, the varible grades is a non-local function, also called upvalues (reminds me of TCL?).

Consider this closure example

function newCounter()
    local i = 0
    return function ()
        i = i + 1
            return i
        end
end
c1 = newCounter()
print(c1()) -- 1
print(c2()) -- 2
c2 = newCounter()
print(c2()) -- 1
print(c1()) -- 3

Each instantiation of the function keeps its own closure acting over the variable.

Closures can be used to create sandboxes.

TOREAD: tail calls. again

7. Iterators and generic for

An iterator is any construction that allows you to iterate over the contents of a collection. In Lua iteators are functions. Everytime you call the iterator, it returns the next element from the collection.

Closure provide the mechanism for store the state between succesive calls to the iterator.

function values(t)
    local i = 0
    return function () i = i + 1; return t[i] end
end

The above iterator returns only the values of a list, unlike ipairs(lst).

In the above, values is a factory, everytime you call this factory, it creates a new closure.

8. Tables

Tables are associative arrays. The index can be any value except nil.

Tables grow dynamically. Tables are objects.

Python has lists, tuples, dictionaries etc., In Lua, you make do with tables.

{tbls.lua 8}

x = {} -- this is a constructor expression

-- tables are anonymous
-- below x and y point to the same table
x[0] = 99
print(x[0])
y = x
y[0] = 100
print(y[0])
print(x[0])

-- Lua's garbage collector will delete the table and reuse its memory when there are no more references to the table.

-- you can use the dot notation to access the values in the table, but only if they are string indexes.

x['foo'] = 123
print(x.foo)



-- this is a table that looks like a python list
lst = {1,2,3,9}

-- to iterate over it and print it, you use the
-- `ipairs` function. become familiar with it, you will be using it a lot.
-- ipairs stands for inde pairs?
-- ipair gives to you the index, and the value

for i, v in ipairs(lst) do print(i, v) end

-- a table can also hold heterogenous types
stock = {"GOOG", "2020-05-29", 1426.82}
for i, v in ipairs(stock) do print (i, v) end


-- a table is really a dictionary
me = {} -- an empty table
me['name'] = 'Pradeep'
me['kids'] = 2

-- unlike python, there is no easy way to inspect the contents
-- of this dict^H table in the REPL
me
-- prints -- table: 0x7f84ae601100
-- unlike Python

[[
>>> me = {}
>>> me['name'] = 'Pradeep'
>>> me['kids'] = 2
>>> me
{'name': 'Pradeep', 'kids': 2}
]]

days = {"Sunday", "Monday"}
print(days[1]) -- Sunday
-- because indexes start with 1 in Lua
-- to override this behaviour
days = {[0]="Sunday", "Monday"}
print(days[0])
-- shortcut way to init a table
a = {x=10, y=20}
print(a.x)

-- trailing commas do not hurt
f = {1,2,}
g = {x=10, y=20, "hello", "world"}
print(g[1]) -- hello
print(g[2]) -- world

9. Strings

Strings are sequence of characters. String are immutable. The escape sequences are familiar:

\a \b \f \n \r \t \v \\ \" \'

10. String manipulation

{string_manip.lua 10}
-- Python code
[[
def get_filename(url):
	"""given https://example.com/foo.html
	return foo.html
	"""
	if url.endswith('.html'):
	    return url.split('/')[-1]
]]

-- Lua does not have a big standard library like Python
-- so something as simple as the above code becomes
-- stackoverflow search :)

-- from https://stackoverflow.com/a/20100401
function split(s, delimiter)
    result = {};
    for match in (s..delimiter):gmatch("(.-)"..delimiter) do
        table.insert(result, match);
    end
    return result;
end

-- I'm going to progressively explore this till I get the
-- above Python program in Lua

-- The above split function returns a lua Table.

split("hello world", " ")
-- table: 0x7ffe977008f0

11. Using Modules

Python comes with a large standard library for everything from dealing with operating system, json, networking, email, http etc

Lua's standad library is not that extensive.

Importing a nonexistant module is a bit more helpful in Lua:

{modules.lua.txt 11}
require 'json'
stdin:1: module 'json' not found:
        no field package.preload['json']
        no file '/usr/local/share/lua/5.3/json.lua'
        no file '/usr/local/share/lua/5.3/json/init.lua'
        no file '/usr/local/lib/lua/5.3/json.lua'
        no file '/usr/local/lib/lua/5.3/json/init.lua'
        no file './json.lua'
        no file './json/init.lua'
        no file '/usr/local/lib/lua/5.3/json.so'
        no file '/usr/local/lib/lua/5.3/loadall.so'
        no file './json.so'
stack traceback:
        [C]: in function 'require'
        stdin:1: in main chunk
        [C]: in ?

Compared to Python:

{modules.py.txt 11}
>>> import foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'foo'

What Lua's error message tells us is:

Luarocks is Lua's package manager.

Once you have that installed [^1], you can install the lua-cjson library. From the name it appears it is a loadable C dynamic library.

{luarocks_install.txt 11}
$ luarocks install lua-cjson
                                                                                                                    1 ↵
Installing https://luarocks.org/lua-cjson-2.1.0.6-1.src.rock

env MACOSX_DEPLOYMENT_TARGET=10.8 gcc -O2 -fPIC -I/usr/local/opt/lua/include/lua5.3 -c lua_cjson.c -o lua_cjson.o
lua_cjson.c:743:19: warning: implicit declaration of function 'lua_objlen' is invalid in C99 [-Wimplicit-function-declaration]
            len = lua_objlen(l, -1);
                  ^
1 warning generated.
env MACOSX_DEPLOYMENT_TARGET=10.8 gcc -O2 -fPIC -I/usr/local/opt/lua/include/lua5.3 -c strbuf.c -o strbuf.o
env MACOSX_DEPLOYMENT_TARGET=10.8 gcc -O2 -fPIC -I/usr/local/opt/lua/include/lua5.3 -c fpconv.c -o fpconv.o
env MACOSX_DEPLOYMENT_TARGET=10.8 gcc -bundle -undefined dynamic_lookup -all_load -o cjson.so lua_cjson.o strbuf.o fpconv.o
lua-cjson 2.1.0.6-1 is now installed in /usr/local (license: MIT)

We can now use this module

{use_cjson.lua 11}
local cjson = require 'cjson'
saturn_moons = {'Titan', 'Mimas', 'Dione', 'Phoebe'}
text = cjson.encode(saturn_moons)
print(text)

12. Colophon

Status: Work In Progress

© 2020, Pradeep Gowda

Written in the literate programming style using Literate

Last-updated: Fri, May 29, 2020