Aller au contenu
Functions that stay small

Functions that stay small

One job per function, names that say what happens, parameters that do not grow, the cost of a function literal in a hot path, and annotating exports.

Cette documentation est en anglais pour l'instant.

Last updated

A function that does one thing is easy to name, easy to test, and easy to delete. A function that does four things has a name like handlePlayer and you have to read all of it before you can change any of it.

One job, one level

Two rules cover most of it. A function should do one thing, and it should stay at one level of detail. Mixing “decide what happens” with “poke the game” is what turns a 20 line function into a 200 line one:

client.lua (two jobs, two levels)
local function useItem(item)
    if item.type == 'food' then
        local ped = PlayerPedId()
        RequestAnimDict('mp_player_inteat@burger')
        while not HasAnimDictLoaded('mp_player_inteat@burger') do Wait(0) end
        TaskPlayAnim(ped, 'mp_player_inteat@burger', 'mp_player_int_eat_burger', 8.0, -8.0, 3000, 49, 0, false, false, false)
        Wait(3000)
        ClearPedTasks(ped)
        TriggerServerEvent('food:server:ateItem', item.name)
    elseif item.type == 'drink' then
        -- another 10 lines of the same
    end
end
client.lua (split)
local function playAnim(dict, clip, duration)
    lib.requestAnimDict(dict)
    TaskPlayAnim(PlayerPedId(), dict, clip, 8.0, -8.0, duration, 49, 0, false, false, false)
    Wait(duration)
    ClearPedTasks(PlayerPedId())
end

local function eat(item)
    playAnim('mp_player_inteat@burger', 'mp_player_int_eat_burger', 3000)
    TriggerServerEvent('food:server:ateItem', item.name)
end

local function useItem(item)
    if item.type == 'food' then return eat(item) end
    if item.type == 'drink' then return drink(item) end
end

useItem now reads as a routing table, eat reads as one action, and playAnim is the only place that knows the native signature. When TaskPlayAnim gains another argument, you change one line.

Names

Kind Style Example
Local function camelCase getShopStock, playAnim
Resource global PascalCase GetItemPrice
Export PascalCase exports('GetItemPrice', ...)

Start with a verb. player() tells you nothing, getPlayerData() tells you what comes back and that nothing changes. dropPlayer() tells you something happens. A function whose name needs “and” in it is two functions.

Parameters

Once you are past about three parameters, nobody remembers the order:

Lua
-- What is the fourth argument again?
createCharacter('Ali', 'Demir', 34, true, false, 'tr')

-- Self describing, and adding a field breaks nothing
createCharacter({
    first = 'Ali',
    last = 'Demir',
    age = 34,
    male = true,
    nationality = 'tr',
})

The table form also lets the caller leave things out without counting nils. The cost is one table allocation per call, so in a function you call thousands of times per frame keep the positional form. Everywhere else the readability is worth more.

Required parameters come first, optional ones last. Lua has no way to skip an argument, so doThing(nil, nil, value) is what happens when you get this wrong.

A boolean parameter usually means two functions. notify(text, isError) has two bodies inside one name, and at the call site notify('Saved', false) tells the reader nothing. notifySuccess(text) and notifyError(text) are clearer and each one is shorter. The exception is a flag that really is an option on one behaviour, like alive on a native.

Function literals in hot paths

In Lua 5.4 a function expression creates a new closure value every time that line runs. There is no cache reusing an identical one, so a literal inside a loop or a per frame callback allocates every pass and gives the garbage collector something to clean up.

Lua
-- A new closure every frame
CreateThread(function()
    while true do
        Wait(0)
        drawSomething(function(x) return x * 2 end)
    end
end)

-- One closure, created once
local function double(x) return x * 2 end

CreateThread(function()
    while true do
        Wait(0)
        drawSomething(double)
    end
end)

This matters in loops and callbacks that fire constantly. Passing a literal to something that runs once, like a command handler or a one off timer, is fine and clearer than naming it.

The same applies to closures that capture upvalues you do not need. If the inner function only uses its arguments, lift it out of the enclosing function so it is created once when the file loads.

Return values

Returning several values is normal in Lua and cheaper than building a table for them:

Lua
local function getSpawn()
    return vector3(-269.4, -955.3, 31.2), 205.0
end

local coords, heading = getSpawn()

Inside one resource, returning a table does not copy it, it hands back a reference, so returning a big table to your own code is not expensive. That changes the moment the value crosses into another resource: an export call packs and unpacks its arguments and returns. See Exports and resource boundaries.

Annotate what other people call

Any function other resources call should carry Lua Language Server annotations. With the CfxLua IntelliSense setup from Start developing for FiveM, these turn into autocomplete and warnings for whoever uses your resource:

server/api.lua
---Price of one item for one player, including job discount and zone tax.
---@param item string Item name, e.g. 'water'
---@param job string Job name, or 'unemployed'
---@param zone string Zone id from the config
---@return number price Final price, never below zero
function GetItemPrice(item, job, zone)
    ...
end

exports('GetItemPrice', GetItemPrice)

It costs four lines and removes an entire category of support questions.

Guard clauses

The best tool for keeping a function small is refusing to continue. That has its own page: Conditionals and early returns.

Next: Variables, constants and enums.