Best Practices in Lua
Writing clean, efficient, and maintainable code is essential for professional development. In this lesson, you'll learn Lua best practices covering code style, naming conventions, performance optimization, security, and professional patterns. Let's explore how to write excellent Lua code!
Code Style and Formatting
Indentation and Spacing
-- Good: Consistent 4-space indentation
function calculateTotal(items)
local total = 0
for i, item in ipairs(items) do
if item.price > 0 then
total = total + item.price
end
end
return total
end
-- Bad: Inconsistent indentation
function calculateTotal(items)
local total = 0
for i, item in ipairs(items) do
if item.price > 0 then
total = total + item.price
end
end
return total
end
Line Length
-- Good: Keep lines under 80-100 characters
local function createUser(name, email, age, address, phone)
return {
name = name,
email = email,
age = age,
address = address,
phone = phone
}
end
-- Bad: Very long lines
local function createUser(name, email, age, address, phone) return {name = name, email = email, age = age, address = address, phone = phone} end
Click Run to execute your code
Naming Conventions
Variables and Functions
-- Good: Descriptive, lowercase with underscores
local user_count = 0
local is_active = true
local max_retry_attempts = 3
function calculate_total_price(items)
-- Implementation
end
-- Bad: Unclear, inconsistent
local uc = 0
local IsActive = true
local MAXRETRY = 3
function calcTP(i)
-- Implementation
end
Constants
-- Good: UPPERCASE with underscores
local MAX_CONNECTIONS = 100
local DEFAULT_TIMEOUT = 30
local API_VERSION = "1.0"
-- Bad: Lowercase like variables
local maxConnections = 100
local defaulttimeout = 30
Classes and Modules
-- Good: PascalCase for classes
local UserManager = {}
local DatabaseConnection = {}
local HttpClient = {}
-- Good: lowercase for modules
local string_utils = require("string_utils")
local math_helpers = require("math_helpers")
Always Use Local Variables
-- Good: Local variables
local function processData(data)
local result = {}
local count = 0
for i, item in ipairs(data) do
count = count + 1
table.insert(result, item)
end
return result, count
end
-- Bad: Global variables (pollutes global namespace)
function processData(data)
result = {} -- Global!
count = 0 -- Global!
for i, item in ipairs(data) do
count = count + 1
table.insert(result, item)
end
return result, count
end
local unless you specifically need a global.
Performance Best Practices
Cache Table Lookups
-- Good: Cache frequently accessed values
local function processItems(items)
local insert = table.insert -- Cache table.insert
local result = {}
for i = 1, #items do
insert(result, items[i] * 2)
end
return result
end
-- Less efficient: Repeated table lookups
local function processItems(items)
local result = {}
for i = 1, #items do
table.insert(result, items[i] * 2) -- Looks up table.insert every time
end
return result
end
Use table.concat() for String Building
-- Good: Use table.concat()
local function buildString(parts)
return table.concat(parts, ", ")
end
-- Bad: String concatenation in loop
local function buildString(parts)
local result = ""
for i, part in ipairs(parts) do
result = result .. part .. ", " -- Creates new string each iteration
end
return result
end
Reuse Tables
-- Good: Reuse table
local temp = {}
for i = 1, 1000 do
-- Clear and reuse
for k in pairs(temp) do
temp[k] = nil
end
-- Use temp
end
-- Less efficient: Create new table each iteration
for i = 1, 1000 do
local temp = {} -- New allocation each time
-- Use temp
end
Click Run to execute your code
Error Handling Best Practices
Validate Input Early
-- Good: Validate at function entry
local function divide(a, b)
assert(type(a) == "number", "First argument must be a number")
assert(type(b) == "number", "Second argument must be a number")
assert(b ~= 0, "Cannot divide by zero")
return a / b
end
-- Bad: No validation
local function divide(a, b)
return a / b -- May cause runtime errors
end
Return nil, error for Expected Failures
-- Good: Return nil, error
local function readConfig(filename)
local file, err = io.open(filename, "r")
if not file then
return nil, "Failed to open config: " .. err
end
local content = file:read("*all")
file:close()
return content
end
-- Usage
local config, err = readConfig("config.txt")
if not config then
print("Error:", err)
return
end
Code Organization
Keep Functions Small
-- Good: Small, focused functions
local function validateEmail(email)
return email:match("^[%w.]+@[%w.]+%.%w+$") ~= nil
end
local function validateAge(age)
return type(age) == "number" and age >= 0 and age <= 150
end
local function createUser(name, email, age)
if not validateEmail(email) then
return nil, "Invalid email"
end
if not validateAge(age) then
return nil, "Invalid age"
end
return {name = name, email = email, age = age}
end
-- Bad: One large function
local function createUser(name, email, age)
if not email:match("^[%w.]+@[%w.]+%.%w+$") then
return nil, "Invalid email"
end
if type(age) ~= "number" or age < 0 or age > 150 then
return nil, "Invalid age"
end
-- More validation...
-- More logic...
-- Gets hard to read and maintain
end
Single Responsibility Principle
-- Good: Each function has one responsibility
local function loadData(filename)
-- Only loads data
local file = io.open(filename, "r")
local content = file:read("*all")
file:close()
return content
end
local function parseData(content)
-- Only parses data
local data = {}
for line in content:gmatch("[^\n]+") do
table.insert(data, line)
end
return data
end
local function processData(data)
-- Only processes data
local result = {}
for i, item in ipairs(data) do
table.insert(result, item:upper())
end
return result
end
-- Bad: One function does everything
local function loadAndProcessData(filename)
-- Loads, parses, and processes - too many responsibilities
end
Documentation
--[[
Calculates the total price of items in a shopping cart
@param items table Array of item objects with 'price' and 'quantity' fields
@param discount number Optional discount percentage (0-100)
@return number Total price after discount
@return number Amount saved from discount
]]
local function calculateTotal(items, discount)
discount = discount or 0
local subtotal = 0
for i, item in ipairs(items) do
subtotal = subtotal + (item.price * item.quantity)
end
local discountAmount = subtotal * (discount / 100)
local total = subtotal - discountAmount
return total, discountAmount
end
Click Run to execute your code
Security Best Practices
- Validate all input: Never trust user input
- Avoid load() and loadstring(): Can execute arbitrary code
- Sanitize file paths: Prevent directory traversal
- Use pcall() for untrusted code: Contain errors
- Don't expose debug library: In production
Best Practices Checklist
- Consistent indentation (4 spaces)
- Lines under 80-100 characters
- Meaningful variable names
- Proper spacing around operators
- Always use
local - Descriptive names
- UPPERCASE for constants
- Keep functions small and focused
- Validate input early
- Return nil, error for failures
- Document complex functions
- Cache table lookups
- Use table.concat() for strings
- Reuse tables when possible
- Profile before optimizing
- One module, one responsibility
- Group related functions
- Use meaningful file names
- Keep modules focused
Practice Exercise
Review and improve this code:
Click Run to execute your code
Summary
In this lesson, you learned:
- Code style and formatting guidelines
- Naming conventions for variables, functions, and modules
- Always using local variables
- Performance optimization techniques
- Error handling best practices
- Code organization principles
- Documentation standards
- Security considerations
What's Next?
Congratulations on completing Module 6! You now know how to write professional, maintainable Lua code. In the final module, we'll explore advanced topics including coroutines, file I/O, and performance optimization. Let's finish strong! 🚀
Enjoying these tutorials?