Lua - Style Guide
Style Guide
Master professional Lua coding standards with comprehensive style guidelines, best practices, and code organization principles for maintainable, readable code.
Estimated time: 20-25 minutes
Learning Objectives
- Apply consistent naming conventions and formatting
- Structure code for maximum readability and maintainability
- Follow Lua community best practices and idioms
- Organize projects with proper module structure
- Write self-documenting code with appropriate comments
Naming Conventions
Consistent naming makes code more readable and professional. Follow these conventions for different types of identifiers.
-- Variable and function names: snake_case
local user_name = "alice"
local max_retry_count = 3
local file_path = "/tmp/data.txt"
local function calculate_total_price(items)
local total = 0
for _, item in ipairs(items) do
total = total + item.price
end
return total
end
-- Constants: UPPER_SNAKE_CASE
local MAX_CONNECTIONS = 100
local DEFAULT_TIMEOUT = 30
local API_BASE_URL = "https://api.example.com"
-- Private/internal functions: leading underscore
local function _validate_input(data)
return data and type(data) == "table"
end
local function _internal_helper()
-- Implementation details
end
-- Class names: PascalCase
local HttpClient = {}
HttpClient.__index = HttpClient
function HttpClient.new(base_url)
local self = setmetatable({}, HttpClient)
self.base_url = base_url
self.timeout = DEFAULT_TIMEOUT
return self
end
-- Method names: snake_case
function HttpClient:set_timeout(timeout)
self.timeout = timeout
end
function HttpClient:make_request(endpoint, data)
-- Implementation
end
-- Module names: lowercase with underscores
-- file: json_parser.lua
-- file: http_client.lua
-- file: user_manager.lua
-- Boolean variables: is_, has_, can_, should_ prefixes
local is_valid = true
local has_permission = false
local can_edit = user.role == "admin"
local should_retry = error_count < MAX_RETRY_COUNT
-- Collections: plural nouns
local users = {}
local active_connections = {}
local error_messages = {"Invalid input", "Connection failed"}
-- Avoid abbreviated names unless widely understood
-- Good
local connection = create_connection()
local message = "Hello, World!"
local configuration = load_config()
-- Avoid (unless context is clear)
-- local conn = create_connection()
-- local msg = "Hello, World!"
-- local cfg = load_config()
Code Formatting
Consistent formatting improves code readability. Follow these formatting guidelines for professional-looking code.
-- Indentation: 2 spaces (not tabs)
local function process_data(input)
if not input then
return nil
end
local result = {}
for i, item in ipairs(input) do
if item.active then
table.insert(result, {
id = item.id,
name = item.name,
processed_at = os.time()
})
end
end
return result
end
-- Line length: aim for 80-100 characters
-- Good: readable line
local user = create_user("John Doe", "[email protected]", "admin")
-- Better: break long lines
local user = create_user(
"John Doe",
"[email protected]",
"admin"
)
-- Function calls: consistent spacing
-- Good
local result = math.max(10, 20, 30)
local items = {1, 2, 3, 4, 5}
local config = {host = "localhost", port = 8080}
-- Avoid
-- local result=math.max(10,20,30)
-- local items={1,2,3,4,5}
-- Operators: space around binary operators
local sum = a + b
local is_equal = x == y
local condition = (age >= 18) and (has_license == true)
-- Control structures: consistent spacing
if condition then
do_something()
elseif other_condition then
do_something_else()
else
default_action()
end
-- Loops: consistent formatting
for i = 1, 10 do
process(i)
end
for key, value in pairs(data) do
print(key, value)
end
while not finished do
step()
end
-- Table definitions: align for readability
local user_config = {
name = "Alice Johnson",
email = "[email protected]",
role = "developer",
active = true,
settings = {
theme = "dark",
language = "en",
auto_save = true
}
}
-- Array-like tables: compact or expanded
local colors = {"red", "green", "blue"}
local large_list = {
"item1",
"item2",
"item3",
"item4"
}
-- Function definitions: consistent spacing
local function short_function()
return "result"
end
local function longer_function(param1, param2, param3)
-- Implementation
return param1 + param2 + param3
end
-- Multiline function calls
local result = complex_function(
first_parameter,
second_parameter,
{
option1 = true,
option2 = "value"
}
)
Code Organization
Well-organized code is easier to understand, maintain, and debug. Follow these principles for code structure.
-- File organization: top to bottom
-- 1. Module declaration and dependencies
local json = require("json")
local http = require("http")
local log = require("log")
-- 2. Constants and configuration
local DEFAULT_TIMEOUT = 30
local MAX_RETRIES = 3
local API_VERSION = "v1"
-- 3. Local utility functions
local function _format_url(base, endpoint)
return base .. "/" .. API_VERSION .. "/" .. endpoint
end
local function _handle_error(err)
log.error("API Error: " .. tostring(err))
return nil, err
end
-- 4. Main module/class definition
local ApiClient = {}
ApiClient.__index = ApiClient
-- 5. Constructor
function ApiClient.new(base_url, api_key)
local self = setmetatable({}, ApiClient)
self.base_url = base_url
self.api_key = api_key
self.timeout = DEFAULT_TIMEOUT
return self
end
-- 6. Public methods (grouped by functionality)
-- Configuration methods
function ApiClient:set_timeout(timeout)
self.timeout = timeout
end
function ApiClient:get_timeout()
return self.timeout
end
-- Request methods
function ApiClient:get(endpoint, params)
local url = _format_url(self.base_url, endpoint)
return self:_make_request("GET", url, nil, params)
end
function ApiClient:post(endpoint, data)
local url = _format_url(self.base_url, endpoint)
return self:_make_request("POST", url, data)
end
-- 7. Private methods
function ApiClient:_make_request(method, url, data, params)
local options = {
method = method,
timeout = self.timeout,
headers = {
["Authorization"] = "Bearer " .. self.api_key,
["Content-Type"] = "application/json"
}
}
if data then
options.body = json.encode(data)
end
if params then
url = url .. "?" .. self:_encode_params(params)
end
local response, err = http.request(url, options)
if not response then
return _handle_error(err)
end
return json.decode(response.body)
end
function ApiClient:_encode_params(params)
local parts = {}
for key, value in pairs(params) do
table.insert(parts, key .. "=" .. tostring(value))
end
return table.concat(parts, "&")
end
-- 8. Module export
return ApiClient
-- Separate concerns into different files
-- user_manager.lua - user operations
-- config_loader.lua - configuration management
-- database.lua - database operations
-- utils.lua - utility functions
Documentation and Comments
Good documentation makes code self-explanatory and helps other developers (including future you) understand the codebase.
--[[
User Management Module
This module provides functionality for managing user accounts,
including creation, authentication, and profile management.
Usage:
local UserManager = require("user_manager")
local um = UserManager.new(database_connection)
local user = um:create_user("[email protected]", "password123")
Dependencies:
- bcrypt: for password hashing
- database: for data persistence
Author: Development Team
Version: 2.1.0
]]
local bcrypt = require("bcrypt")
local database = require("database")
local UserManager = {}
UserManager.__index = UserManager
--- Creates a new UserManager instance
-- @param db_connection Active database connection
-- @return UserManager instance
function UserManager.new(db_connection)
local self = setmetatable({}, UserManager)
self.db = db_connection
self.password_rounds = 10 -- bcrypt rounds for password hashing
return self
end
--- Creates a new user account
-- @param email User's email address (must be unique)
-- @param password Plain text password (will be hashed)
-- @param profile Optional user profile data
-- @return User object on success, nil and error message on failure
function UserManager:create_user(email, password, profile)
-- Validate required parameters
if not email or not password then
return nil, "Email and password are required"
end
-- Validate email format
if not self:_is_valid_email(email) then
return nil, "Invalid email format"
end
-- Check if user already exists
local existing_user = self.db:find_user_by_email(email)
if existing_user then
return nil, "User with this email already exists"
end
-- Hash password for security
local password_hash = bcrypt.hash(password, self.password_rounds)
-- Create user record
local user_data = {
email = email,
password_hash = password_hash,
profile = profile or {},
created_at = os.time(),
updated_at = os.time(),
active = true
}
local user_id, err = self.db:insert_user(user_data)
if not user_id then
return nil, "Failed to create user: " .. (err or "unknown error")
end
user_data.id = user_id
return user_data
end
--- Authenticates a user with email and password
-- @param email User's email address
-- @param password Plain text password
-- @return User object on success, nil on failure
function UserManager:authenticate(email, password)
if not email or not password then
return nil
end
local user = self.db:find_user_by_email(email)
if not user or not user.active then
return nil -- User not found or inactive
end
-- Verify password against stored hash
if bcrypt.verify(password, user.password_hash) then
-- Update last login timestamp
self.db:update_user(user.id, {last_login = os.time()})
-- Remove sensitive data before returning
user.password_hash = nil
return user
end
return nil -- Invalid password
end
--- Updates user profile information
-- @param user_id User's unique identifier
-- @param updates Table of fields to update
-- @return true on success, false and error message on failure
function UserManager:update_profile(user_id, updates)
if not user_id or not updates then
return false, "User ID and updates are required"
end
-- Prevent updating sensitive fields
local restricted_fields = {"id", "password_hash", "created_at"}
for _, field in ipairs(restricted_fields) do
if updates[field] then
return false, "Cannot update field: " .. field
end
end
-- Add update timestamp
updates.updated_at = os.time()
local success, err = self.db:update_user(user_id, updates)
if not success then
return false, "Failed to update profile: " .. (err or "unknown error")
end
return true
end
--- Private method to validate email format
-- @param email Email string to validate
-- @return true if valid, false otherwise
function UserManager:_is_valid_email(email)
-- Simple email validation pattern
local pattern = "^[%w%._%+-]+@[%w%.%-]+%.%a%a+$"
return string.match(email, pattern) ~= nil
end
-- Single-line comments for quick explanations
local cache = {} -- In-memory cache for frequent lookups
local max_attempts = 3 -- Maximum login attempts before lockout
-- TODO: Implement password reset functionality
-- FIXME: Handle database connection timeouts
-- NOTE: Consider implementing rate limiting for authentication
return UserManager
Error Handling and Defensive Programming
Write robust code that handles errors gracefully and validates inputs properly.
-- Input validation patterns
local function validate_user_input(user_data)
-- Check required fields
local required_fields = {"name", "email", "age"}
for _, field in ipairs(required_fields) do
if not user_data[field] then
return false, "Missing required field: " .. field
end
end
-- Type validation
if type(user_data.name) ~= "string" or #user_data.name == 0 then
return false, "Name must be a non-empty string"
end
if type(user_data.age) ~= "number" or user_data.age < 0 or user_data.age > 150 then
return false, "Age must be a number between 0 and 150"
end
-- Email format validation
local email_pattern = "^[%w%._%+-]+@[%w%.%-]+%.%a%a+$"
if not string.match(user_data.email, email_pattern) then
return false, "Invalid email format"
end
return true
end
-- Resource management patterns
local function safe_file_operation(filename, operation)
local file, err = io.open(filename, "r")
if not file then
return nil, "Failed to open file: " .. (err or "unknown error")
end
-- Ensure file is closed even if operation fails
local success, result = pcall(operation, file)
file:close()
if success then
return result
else
return nil, "Operation failed: " .. tostring(result)
end
end
-- Usage example
local function read_config(filename)
return safe_file_operation(filename, function(file)
local content = file:read("*a")
return parse_config(content)
end)
end
-- Nil-safe operations
local function safe_get_nested_value(table, ...)
local current = table
local keys = {...}
for _, key in ipairs(keys) do
if type(current) ~= "table" or current[key] == nil then
return nil
end
current = current[key]
end
return current
end
-- Usage: safely access deeply nested values
local value = safe_get_nested_value(data, "user", "profile", "settings", "theme")
-- Assertion patterns for debugging
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, "Division by zero is not allowed")
return a / b
end
-- Error propagation patterns
local function process_user_data(raw_data)
-- Validate input
local is_valid, error_message = validate_user_input(raw_data)
if not is_valid then
return nil, "Validation failed: " .. error_message
end
-- Process data (might fail)
local processed_data, process_error = transform_data(raw_data)
if not processed_data then
return nil, "Processing failed: " .. process_error
end
-- Save to database (might fail)
local success, save_error = save_to_database(processed_data)
if not success then
return nil, "Save failed: " .. save_error
end
return processed_data
end
-- Retry patterns with exponential backoff
local function retry_operation(operation, max_attempts, base_delay)
max_attempts = max_attempts or 3
base_delay = base_delay or 1
for attempt = 1, max_attempts do
local success, result = pcall(operation)
if success then
return result
end
if attempt < max_attempts then
local delay = base_delay * (2 ^ (attempt - 1))
-- In real code, implement actual sleep
print(string.format("Attempt %d failed, retrying in %d seconds...",
attempt, delay))
end
end
error("All retry attempts failed")
end
Performance Best Practices
Write efficient Lua code by following performance guidelines and avoiding common pitfalls.
-- String concatenation: use table.concat for multiple strings
-- Inefficient: repeated string concatenation
local function build_html_slow(items)
local html = ""
for _, item in ipairs(items) do
html = html .. "" .. item .. " " -- Creates new string each time
end
return html
end
-- Efficient: table.concat approach
local function build_html_fast(items)
local parts = {}
for _, item in ipairs(items) do
table.insert(parts, "" .. item .. " ")
end
return table.concat(parts)
end
-- Local variables: faster access than globals
local math_max = math.max -- Cache frequently used functions
local string_format = string.format
local function process_numbers(numbers)
local max_value = 0
for _, num in ipairs(numbers) do
max_value = math_max(max_value, num) -- Use cached local function
end
return max_value
end
-- Table preallocation: reserve space when size is known
local function create_sequence(n)
local sequence = {}
-- Pre-allocate if you know the size
for i = 1, n do
sequence[i] = i * i
end
return sequence
end
-- Avoid table creation in loops
-- Inefficient: creates new table each iteration
local function process_data_slow(items)
local results = {}
for _, item in ipairs(items) do
local temp = {value = item, processed = true} -- New table each time
table.insert(results, temp)
end
return results
end
-- Better: reuse table or create once
local function process_data_fast(items)
local results = {}
for i, item in ipairs(items) do
results[i] = {value = item, processed = true} -- Direct assignment
end
return results
end
-- Numeric for loops: faster than generic for
-- Use numeric for when working with arrays
local function sum_array_fast(arr)
local sum = 0
for i = 1, #arr do -- Numeric for loop
sum = sum + arr[i]
end
return sum
end
-- Generic for is fine for key-value iteration
local function sum_values(hash_table)
local sum = 0
for key, value in pairs(hash_table) do
if type(value) == "number" then
sum = sum + value
end
end
return sum
end
-- Function call overhead: consider inlining for hot paths
-- Function call version
local function calculate_distance(x1, y1, x2, y2)
local dx = x2 - x1
local dy = y2 - y1
return math.sqrt(dx * dx + dy * dy)
end
-- Inlined version for performance-critical code
local function update_particles(particles)
for i = 1, #particles do
local p = particles[i]
-- Inline distance calculation to avoid function call overhead
local dx = p.target_x - p.x
local dy = p.target_y - p.y
local distance = math.sqrt(dx * dx + dy * dy)
if distance > 0.1 then
p.x = p.x + dx * 0.1
p.y = p.y + dy * 0.1
end
end
end
-- Memory management: avoid creating garbage
-- Use object pools for frequently created/destroyed objects
local ParticlePool = {
available = {},
active = {}
}
function ParticlePool:get()
local particle = table.remove(self.available)
if not particle then
particle = {x = 0, y = 0, vx = 0, vy = 0} -- Create new if pool empty
end
table.insert(self.active, particle)
return particle
end
function ParticlePool:release(particle)
-- Find and remove from active list
for i, p in ipairs(self.active) do
if p == particle then
table.remove(self.active, i)
break
end
end
-- Reset and return to pool
particle.x, particle.y = 0, 0
particle.vx, particle.vy = 0, 0
table.insert(self.available, particle)
end
Common Pitfalls and Anti-patterns
Avoid These Common Mistakes
- Global variables: Use
local
to avoid polluting global namespace - Magic numbers: Replace numeric literals with named constants
- Deep nesting: Use early returns to reduce nesting levels
- Long functions: Break complex functions into smaller, focused ones
- Inconsistent style: Follow consistent formatting throughout codebase
-- Anti-pattern: Global variables
-- count = 0 -- Global variable (bad)
local count = 0 -- Local variable (good)
-- Anti-pattern: Magic numbers
-- if user.age >= 18 then -- What does 18 mean?
local LEGAL_AGE = 18
if user.age >= LEGAL_AGE then -- Clear intent
-- Anti-pattern: Deep nesting
-- Avoid deep nesting
local function process_user_bad(user)
if user then
if user.active then
if user.email then
if validate_email(user.email) then
-- Process user
return true
else
return false, "Invalid email"
end
else
return false, "No email"
end
else
return false, "User inactive"
end
else
return false, "No user"
end
end
-- Better: Early returns
local function process_user_good(user)
if not user then
return false, "No user"
end
if not user.active then
return false, "User inactive"
end
if not user.email then
return false, "No email"
end
if not validate_email(user.email) then
return false, "Invalid email"
end
-- Process user
return true
end
Checks for Understanding
- Why should you use snake_case for variables in Lua?
- When should you use leading underscores in function names?
- What's the recommended indentation size for Lua code?
- How can you make string concatenation more efficient?
Show answers
- It's the widely adopted convention in the Lua community for consistency
- For private/internal functions that shouldn't be called externally
- 2 spaces (not tabs) for consistency and readability
- Use table.concat() instead of repeated string concatenation
Style Checklist
- ✓ Use snake_case for variables and functions
- ✓ Use PascalCase for classes/modules
- ✓ Use UPPER_SNAKE_CASE for constants
- ✓ Include meaningful comments and documentation
- ✓ Validate inputs and handle errors gracefully
- ✓ Organize code logically with consistent formatting
🎉 Congratulations!
You've completed the comprehensive Lua programming tutorial covering 23 topics from beginner to architect level. You now have the knowledge to build robust, maintainable Lua applications using professional coding practices.
What's Next?
- 🔄 Review fundamental concepts
- 🚀 Start building your own Lua projects
- 📚 Explore advanced topics like LuaJIT and C integration
- 🌟 Contribute to open source Lua projects