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

    1. Why should you use snake_case for variables in Lua?
    2. When should you use leading underscores in function names?
    3. What's the recommended indentation size for Lua code?
    4. How can you make string concatenation more efficient?
    Show answers
    1. It's the widely adopted convention in the Lua community for consistency
    2. For private/internal functions that shouldn't be called externally
    3. 2 spaces (not tabs) for consistency and readability
    4. Use table.concat() instead of repeated string concatenation

    Style Checklist

    1. ✓ Use snake_case for variables and functions
    2. ✓ Use PascalCase for classes/modules
    3. ✓ Use UPPER_SNAKE_CASE for constants
    4. ✓ Include meaningful comments and documentation
    5. ✓ Validate inputs and handle errors gracefully
    6. ✓ 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