Lua general programming tips


  1. Introduction
  2. General rules
    1. Adding new features (YAGNI)
    2. Writing simple solutions (KISS, DTSTTCPW)
    3. Re-factoring all the time ("Boy scout" rule)
  3. Best practices
    1. One statement per line
    2. Combining function calls
    3. Math instead of conditionals
    4. Avoiding intermediate objects


This is not really a tutorial, but merely an eclectic (and perhaps somewhat subjective) collection of tips for writing better Lua code.

General rules

Adding new features (YAGNI)

This rule refers to programming features that you plan on using in the future. The rule of thumb is: "You aren't gonna need it" (YAGNI). You shouldn't add new functionality or features to your code until they are actually necessary. Any new feature imposes constraints on what can be done in the future, so an unnecessary feature may preclude needed features from being added later on. Until the feature is actually needed, it is difficult to fully define what it should do and how to test it.

Writing simple solutions (KISS, DTSTTCPW)

Simple code is elegant, bug free and easy to build upon. These two rules are often mentioned among programmers: "Keep it simple, stupid" (KISS) and "Do the simplest thing that could possibly work" (DTSTTCPW).

Re-factoring all the time ("Boy scout" rule)

Every time you edit existing code, try to leave it a little bit better than it was before ("Boy scout" rule). Keep your code clean and concise so it is easier to understand, modify, and extend. Make sure everything is expressed once and only once.

Best practices

One statement per line

The temptation of writing fewer lines of code often leads programmers to cram multiple statement in a single line. Sure, the final script may have fewer lines, but the resulting code becomes harder to work with. Apart from the potential inconvenience of having to scroll the code horizontally there's other legibility issues as well. When looking at a Lua script, it should be possible to describe what each line of the code does in a single sentence. Code that does several things on a single line is usually harder to read, maintain and optimize.

Having fewer lines does not necessarily result in faster code. Can you spot the two extra operations in the first example?
-- example 1
local d = math.sqrt((x - x2)^2 + (y - y2)^2)
local nx, ny = (x - x2)/d, (y - y2)/d
-- example 2
local dx, dy = x - x2, y - y2
local dsq = dx^2 + dy^2
local d = math.sqrt(dsq)
local nx, ny = dx/d, dy/d

Combining function calls

Like most interpreted languages, Lua does not have function declarations. Therefore, changing the name of a function or its requisite number of arguments is dangerous and time consuming. Once you've modified a particular function, you have to manually update all possible evocations of that function in your code. Errors can often slip by and remain unnoticed until a bad function call is made at runtime. This is why it is better to avoid duplicated function calls in your code as much as possible. This rule of thumb is especially important when working with a third party API or Lua modules. Fewer function calls between our Lua code and its dependencies means more flexibility when porting to a different or an updated API.

The first example has four duplicate function calls.
-- example 1
if up then
  player:move(0, 10)
elseif down then
  player:move(0, -10)
if right then
  player:move(10, 0)
elseif left then
  player:move(-10, 0)
-- example 2
local dx, dy = 0, 0
if up then
  dy = 10
elseif down then
  dy = -10
if right then
  dx = 10
elseif left then
  dx = -10
if dx ~= 0 or dy ~= 0 then
  player:move(dx, dy)

Math instead of conditionals

A little bit of math knowledge can often replace a lot of programming work. There are several reasons why math equations are superior in solving problems as opposed to "if" statements. Functions that only contain equations can easily be rearranged to return an inverted value. On the other hand, code that has a lot of conditional statements is less flexible and more prone to bugs.

This example converts an index from a scattered tilemap to screen position.
-- if then solution
local x = tx*tile_width
if ty%2 == 1 then
  x = x + tile_width/2
local y = ty*tile_height/2
-- math solution
local x_offset = ty%2*(tile_width/2)
local x = tx*tile_width + x_offset
local y = ty*(tile_height/2)

Avoiding intermediate objects

In my experience, this is probably the most important optimization tip when writing Lua code. Creating tables all the time is bad not only because of the time required to allocate each new table instance. By constantly creating new tables, you are putting a strain on the garbage collector too. It might mean the difference between a game that runs at a consistent frame rate versus a game that stutters every few seconds when a garbage collection cycle occurs.

In Lua, functions can return multiple values often circumventing the need of creating intermediate table objects.
-- example 1
player.get_position = function(player)
  return { x = player.x, y = player.y }
-- example 2
player.get_position = function(player)
  return player.x, player.y