Try   HackMD

4thAxis' Luau Style Guide

Introduction and Overview

First and foremost before we get started please remember this is not the official style guide of Lua or Luau.

The purpose of following a style guide is to keep consistent and clean code. You may choose to follow or take inspiration or if you're not here to read about style practices, you may simply read this for enlightenment. This guide assumes you have a general understanding of Luau. This guide also goes into depth into why you should follow such practices. The goal of this style guide is to promote styles that respect the modified LSP of Lua 5.1 external Luau-like analysis that are also in regards to developer ergonomics. This guide values styles that promote readability and in a language that is being constantly optimized the era of focusing on performance over readability is slowly coming to an end.

This guide at the moment does not support static type checking.

Table of Contents

Comments

Writing comments

Single line comments are to have spaces after the double hyphen like so:

-- Line Comment here

Block/body comments are supposed to be formatted like so:

--[[
    Two dashes with two square brackets and enter.
]]

When and where

Comments should be kept minimum, provide comments only when needed. For example describing a series of semantics used to achieve something and when your code doesn’t express those semantics clearly is a good example when to provide comments. Otherwise you shouldn’t. Providing more comments makes code much more harder to read. Though this seems like a very contradicting statement it’s true. What takes more longer to read, a 5 paragraph essay or a 1 paragraph essay? Many famous authors like Robert C. Martin have mentioned this. In Short, if you find yourself practicing amplification on comments, you’re doing something wrong. [3]

Code should be self-documenting, however, open-source APIs follow different rules. You should be documenting your APIs well with comments to further abstract your code. The user shouldn’t have to read your API code to figure out how to use it.

Comments should always be placed inside scopes. Most code editors and IDEs will close out scopes and you can use this to your advantage to hide comments.

Variables

Scope Bounding

Localization

Always scope your variables locally. Not only are the usage of global variables in Luau very slow but much more confusing. There are times where you can get reassignment confused with the declaration of a global. You’ll also end up using more resources and because of this, you’ll have to come up with silly variable names like: Jump2 or NewJump because the previous variables couldn’t be garbage collected. In short, there is never a real reason to have a global avoid this at all times.

Under The Hood, Special Instructions

Unlike globals, local variables don't require any special instructions. From an internal-view Luau (at the time this guide was written) generates the same instructions as native Lua 5.1, SETGLOBAL for defining a global and GETGLOBAL for fetching a global. Globals are stored on the environment and fetching these takes much longer. Globals are fetched with a dictionary look-up on the environment while locals only require to be indexed from an array stored on the stack in some register. Globals are also never garbage collected. This also at the time of this writing conflict with parallel Luau as the environment is only isolated to the VM it's running in, therefore this will break dependencies unless Luau adds some secure shared storage between multiple VMs that still follow the principles of VM isolation for Luau sandboxing.

Naming Convention

Diversity

This style guide unlike most guidelines doesn't follow multiple casing. You should use only one casing style preferably PascalCase and for everything. The idea of having specific casings for specific variables is to denote a certain variable type (like an argument), however, this has problems.

The problem with different casing patterns:

  1. More diversity in casing will make code look inconsistent.
  2. The programmer has to continuously reference back to the casing guide for certain variables.
  3. The reader would also find this confusing if the casing guide wasn't linked and if it was, the reader would also have to continuously reference the guide for context.

The Mono-Belief

A simple solution is naming your variables correctly in the first place give proper meaning to your variables instead of relying on specific casings for variables. Using camelCase in Luau is useless and I find this especially more inconsistent (no disrespect to those who use this casing). The casing is understandable for object-oriented programming in other languages but Luau doesn't implement built-in classes or an inheritance system (at the time this guide was written).

Arrangement

An Ergonomically-driven Style

The arrangement not only follows a near universal standard of variable arrangement but goes into depth promoting developer ergonomic benefits to help you write better and more readable code.

Modules

Ideally, you should always require modules first since standardly in most programming languages you always import/load/require modules first.

Services

After modules in a new section, you fetch services. Sometimes services may have to be declared before modules. Always use GetService().

Assets

After services in a new section, you fetch and store assets using WaitForChild.

Modifiers

After assets in a new section, you store constants and setting variables that are meant to modify your script

Run-time Variables

After settings in a new section you store variables that your code will use and modify at run-time.

Tables

Tables are meant to go last, after regular variables all in the same section or if needed, a new section

Functions

After your variables you have your functions; arranged from auxiliary functions that are least commonly called to most commonly. Right after your auxiliary should come your main functions.

Format

Here is an example of how variables should be formatted (Commenting out sections are optional, I did this for a clearer visual representation):


-- Modules
local Module = require(:WaitForChild("Module"))
local Module2 = require(:WaitForChild("Module2"))
local Module3 = require(:WaitForChild("Module3"))
 
-- Service
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local RunService = game:GetService("RunService")

-- Assets 
local Effect = :WaitForChild("Effect")
local Shockwave = :WaitForChild("Shockwave")
local Particle = :WaitForChild("Particle")

-- Settings
local PartSpawningSpeed = 100
local PartSize = 1
local WindSpeed = 20
 
-- Variables
local Name = "Steve"
local CurrentItem = "Sword"
local Table = { "Harris, Thomas, Kevin" }
local TemporaryTable = {} -- { "Billy, Newton" }

-- Auxiliary
local function ResetPlayer()
    ...
end


local function ConvertTo...()
    return ...
end

-- Main functions
local function FireballHandler()

end

...

Variable Alignment

Vertical alignment and both regular alignments are completely fine. Personally I find vertical alignment more pleasing to read, especially when you have a lot of defined variables. Most IDEs have shortcuts to automatically vertically align your code and this style is totally acceptable by Luau’s flexible lexer and linter. Although you will have to stick to one alignment style for consistency.

Assessment

Idiomatic Evaluation

When assessing variables you should do this in an idiomatic matter. Use the not operator instead of ~= nil/ ~= false. Only use ~= nil when you strictly have to check if the variable is nil. Do if Exist then instead of if Exist == true then.

Good:

if Running then

end

if not Running then

end

Bad:

if Running == true then

end

if Running == false then

end

When necessary:

if Existance == nil then

end

Internal semantics, who wins?

Internally, the idiomatic convention generates a different bytecode compared to the strict imperative evaluation: Luau (at the time of this writing) still uses native Lua 5.1’s instruction for handling these types of semantics. The instruction: TEST for the mentioned assessment which proves to be faster than the instruction for the non-idiomatic evaluation: EQ. Lua’s VM will execute the TEST instruction in conjunction with the JMP instruction to implement short-circuit LISP-style logical operators that retain and propagate operand values instead of booleans. Though the performance differences between the latter is quite miniscule, this settles arguments for those who argue over what’s the faster method.

Source: https://www.lua.org/source/5.1/lopcodes.c.html
Source: https://www.lua.org/source/5.1/lvm.c.html

Dummy Variables convention

This style guide follows the standard convention for dummy variables like so:

local _ = ...

Syntax

Stay consistent with syntax and follow the rules!

Literals

When declaring a string literal, use double quotes: " "

Function call literals

Reasonability

For consistency reasons, you should always use the syntax: Call() to call a function no matter the micro-performance boost. In some cases for readability purposes it is reasonable to use the syntax: Call{}. Here's a good example of implementing a table call expression:

Dialogues.Bob = {}; Dialogues.Bob.Dialogues = {

	DogDialogue = Dialogue.new("Find my dog."): Choices {
		
		["Yes"] = Dialogue.new("Thanks for accepting!").DialogueEvents:AssignQuestTo("4thAxis", "Lost Dog"),
		["No"] = Dialogue.new("Aww...").DialogueEvents:RemoveQuestFrom("4thAxis", "Lost Dog"),

		["Here you are!"] = Dialogue.new("Thanks so much!").DialogueEvents:GiveRewards()
	}
}

Only-exception

Unfortunately, the only exception besides the default call-statement was the table-call this means that the string-call expression is not favored by the style guide. For one, the syntax is not appealing cognitively and two, not many people use the syntax which brings inconsistencies.

Abstract Syntax Tree Accounts

The aforementioned micro-performance benefits are only the result of a more compact AST production. The bytecode generated will be the same however the tokens are being assembled more differently into an abstract syntax tree. The difference is that the argument node doesn’t build sub-nodes for more arguments instead it re-uses the same argument node to place the literal argument set. Remember native Lua does not have a AST generation phase as it uses a one-pass compiler in contrast to Luau, however, there are AST inspection tools that you can use. It seems this optimization is applied by those ASTs inspectors commonly so it could be something that Luau would apply which would explain for the almost unnoticeable speed differences. Here is the AST format below:

local function Hello(TableArgument) end

Hello({})

Hello{}

Generates the following AST (https://astexplorer.net/):

- Chunk {
    type: "Chunk"
    - body: [
        + FunctionDeclaration {} -- Hello function
        
        - CallStatement {
            type: "CallStatement"
            
            - expression: CallExpression {
                type: "CallExpression"
                + Base: Identifier {}
                - arguments: [ -- Argument node 
                    - TableConstructorExpression { -- sub-nodes for arguments passed, +1 for each argument.
                        type: TableConstructorExpression
                        fields: [ ]
                        + range
                    }
                ]
            }
        }
        
         - CallStatement {
            type: "CallStatement"
            
            - expression: TableCallExpression {
                type: "TableCallExpression"
                + Base: Identifier {}
                - arguments: TableConstructorExpression [ -- re-use argument node here and not build any sub-nodes.
                
                    type: TableConstructorExpression
                    fields: [ ]
                    + range
                    
                ]
            }
        }
    ]
}

The benefit is that during the sementical phase of compilation, we only traverse less nodes.

Whitespacing

You should be indenting code standardly or stylistically (which will be discussed later on) for each scope. Leave whitespaces to section off code. Here’s a good example of separating stuff with whitespacing:


local function Dialogue(Player, Npc)
	local LastDialogue = Player.LastCompletedDialogues.Of(Npc)
	
	if LastDialogue then
		return ( next(Npc.Dialogues, LastDialogue) or "Thanks for all your help!" ) 
	else
		return Npc.Dialogue1
	end
end


Bob.Interact.PromptTriggered:Connect(function(_, Player)
	Bob.Prompt( Dialoge(Player, Bob) )
	
	local Connection; Connection = Player.QuestAccepted:Connect(function(Quest)
		Player.CurrentQuest = Quest; Connection = nil
	end)
end)

-- Example 2

function OrbitSystem:_Orbit()
    local Orbiters = self.Orbiters
    
    local OrbiterName, CurrentOrbiter, OrbiterAngle, OrbiterRadius, OrbitSpeed, OrbitClockwise;
    local PreviousOrbiter
    
    RunService.Stepped:Connect(function()       
        if self.Pause then return end
        
        OrbiterName = next(Orbiters, PreviousOrbiter) or next(Orbiters); CurrentOrbiter = Orbiters[OrbiterName] do 
 
            OrbiterRadius    = CurrentOrbiter.Radius
            OrbitSpeed       = CurrentOrbiter.OrbitSpeed
            OrbitClockwise   = CurrentOrbiter.Clockwise
            OrbiterAngle     = CurrentOrbiter.Angle 
        end
        
 
        CurrentOrbiter.Object.Position = Vector3.new (
            
            self.Center.Position.X + math.cos(OrbiterAngle) * CurrentOrbiter.Radius, 
            0, 
            self.Center.Position.Z + math.sin(OrbiterAngle) * CurrentOrbiter.Radius
        )
        
        CurrentOrbiter.Angle += ( OrbitClockwise and OrbitSpeed ) or -(OrbitSpeed)
        PreviousOrbiter = OrbiterName
        
    end)
end

Punctuation

Don’t use semicolons for every statement to show a line has ended the lexer is flexible enough to generate tokens and lexemes correctly. Only use semicolons when you have multiple statements in a single line. Here’s an example:

local Object = instance.new("Part"); Object.Parent = ...

...

The style guideline does promote the use of multiple statements in a single line but only to a certain extent. It's a good way to group code into sections and visually reduce your LOC.

When you have more complex expressions or statements the ones that especially span over lines are the ones where you should optionally use semicolons like so:

local Vector = Vector3.new (
    10,
    10,
    10
); -- Optional

local _ = ( ... and ... ) or ( ... and ... ); -- Optional



if ... then 
    CallFunction();   -- Bad
    Value += 1;       -- Bad
end

Expressions

Conditional Expressions

Expressions must be constructed in single statements like so:

if Running and Sitting then
    
end

Paranthesis shouldn't be used in if-statements unless you have ternary operators which will be discussed later on.

The Anti-pattern

if not Running and Sitting
   or Sitting
   or ...
   or ... then
   
end

The shown pattern above is interpreted by many as a code smell. The modified Lua 5.1 language server for external Luau-like analysis in IDEs won’t function properly with this. This code smell also exists in other languages and IDEs like JetBrains will flag this. Remember this style guide respects Luau’s linter analysis’s rules and all mentioned styles are in regard to proper linter functionality.

Conditional Branching

Conditional branching is the biggest way to reduce readability besides bad variable naming this means that you should keep branching to a minimum when possible to lower the cognitive complexity of your code [2]. If needed you may take the inverse of your condition to optimize the cognitive complexity like so:

local Profile = ...

    if Profile then

        profile:ListenToRelease(function()
            CachedProfiles[Player] = nil
            PlayerData[Player] = nil
            player:Kick(...)
        end)

        if Player:IsDescendantOf(Players) then
            CachedProfiles[Player] = Profile

            local Data = CachedProfiles[Player]

            PlayerData[Player] = {...}
        else
            Profile:Release()
        end
    else
        Player:Kick(...)
    end

This can be optimized by preserving the semantics and only taking the inverse of a few conditions to generate less branching and LOC. In many times, we can use this to optimize the control-flow graph and produce less edge cases and inherently reduce quantitative metrics measuring the complexity of our code like so:

if not Profile then Player:Kick() end

Profile:ListenToRelease(function()
    CachedProfiles[Player] = nil
    PlayerData[Player] = nil
    Player:Kick(...)
end)

if not Player:IsDescendantOf(Players) then profile:Release() end
CachedProfiles[Player] = Profile

local Data = CachedProfiles[player]
PlayerData[player] = {...}

In-line conditionals are completely allowed and encouraged as again our flexible lexer will generate tokens just fine along with regards to static analysis. A general rule is that in-line if-statements should be avoided if they become too long. This is determined intuitively by the programmer.

Boolean Semantics

Null-Coalescing and Ternary Optimization

Short-circuit evaluation also known as “minimal evaluation”, is another practice used to create more readable code this will not raise the cognitive complexity of your code:

Cognitive Complexity also ignores the null-coalescing operators found in many languages,
again because they allow short-handing multiple lines of code into one. For example, both of
the following code samples do the same thing:

MyObj myObj = null;        MyObj myObj = a?.myObj;
if (a != null) {
 myObj = a.myObj;
}                    

The meaning of the version on the left takes a moment to process, while the version on the
right is immediately clear once you understand the null-coalescing syntax. For that reason,
Cognitive Complexity ignores null-coalescing operators.

G. Ann Campbell. 2018. Cognitive Complexity - A new way of measuring understandability. Technical Report. SonarSource SA, Switzerland. https://www.sonarsource.com/docs/CognitiveComplexity.pdf

Ternary Semantics Syntax

Ternary operators can become harder to read which is why it is important to provide parenthesis like so:

local WalkingSpeed = ( Healthy and Fast ) or Slow

local ... = ( Healthy and Running ) or ( BadHealth and Sitting )

Bad:

local WalkingSpeed = Healthy and Fast or Slow

local ... = Healthy and Running or BadHealth and Sitting -- Way to confusing to read

Parentheses Rule

Parentheses should only be spaced in between if your parentheses are nested like so:

local ... = Call( Vector3.new() )

Precedence (TODO)

Tables

Structuring

Table elements must be separated only by commas. Semicolons are discouraged and bad practice as they are used instead to show a line has ended. This may also cause confusion in table arrangement so it’s best to steer away and stick to consistent code.

Linear Arrays that hold only indices must be structured like so:

local Array = {1, 2, 3, 4, 5, 6, 7, 9, 10}
local FriendList = {"James", "Hairy", "Bob", "Newton"}

If your Array holds too many indices to be grouped into a single line you should make them multi-lined like so:

local MyManyFriends = {
    "Sven",
    "Armstrong",
    "Roberto",
    "John",
    "Gavin",
    "Lucy",
    "Kevin",
    "Bob"
}

Dictionary-tables are structured like so and will always be multi-lined:

local Physics = {
    Inertia = 5,
    RotationalInertia = Vector3.new(),
    Gyro = Vector3.new(4000, 0, 4000)
    
    MobilityForce = {
        Forward = 3000,
        Left = 1500,
        Right = 1500,
        Backwards = 3000
    }
}

If you find yourself implementing data structures in tables you must have names referring to their abstract data types somewhere in the name of your table variable, like so:

local WeaponTree = {}

local DialogueGraph = {}

local EventQueue = {}

Unoccupied Tables

When you construct a table and later populate you should comment future elements like so:

local HitList = {} -- { Players }
local GetFriends = {} -- { Bob, Sam, Jimmy }

-- So we know what elements will be appended to our table for further context.

Indexing

When possible you should always be indexing your tables with the following notation:

Table.Element

Over

Table["Element"]

Indexing Optimization

As of the writing of this style guide, Luau only does inline caching for the first shown example. Luau implements this using a mechanism that is used by many VMs like JVM and LuaJIT. The compiler can predict the hash slot used by field lookup, and the VM can correct this prediction dynamically [1].

Abstraction

If your Tables take up too much space it is best to abstract them into modules which should preferably be placed underneath your script or module. Less code is always better code.

Functions

Structure

Your functions should be structured consistently like so:

  1. Comments first!
  2. Guard Statements, returns that have to be done on certain conditions to stop your function from running.
  3. Argument Checks.
  4. Variables.
  5. Main logic.
  6. Only return if needed. specifying no returns by returning nil is more to read.

Example:

local function Run(Player, Speed)
--[[
    Comments about the function here if necessary. Putting comments inside the function
    is better because most IDEs allow you to close out scopes so we can close out long comments too!
]]

-- Guard Statement first, try to keep this an in-line statement if possible:
if not Healthy return end

-- Argument checks next:
Player = Player or error(...)
Speed = Speed or 10

-- Variables:
local Humanoid = ...
local ... = ...

-- Main Logic:

...

if ... then
    ...
end

...

end

Monadic, Dyadic, Triad forms (TODO)

Parameters

When you reach over 5 arguments to your functions you should use tables instead. As your parameters grow it increases the chances of parameter positional mismatch which is why you should take the aforementioned developer ergonomic approach. You’ll also actually save VM registers.

Function parameters should be commented out with their types like so (if you don't use Luau's static type checking system):

local function Run(Speed, Duration) -- Run ( Speed: number, Duration: number )

end

-- Varags should be styled like so:

-- Function ( Argument: string, (single name here that regards to the varags): ... ) 

Exactly as formatted above.

Error Handling

You should always be doing proper error handling to keep efficient maintainability of your code.

Dynamic checks

This guide is against the assert function. Simple short circuit evaluations can be used which look much more readable like so:

Player = Player or error("The player does not exist :(")
-- vs
assert(Player, "The player does not exist")

Internally, assert handles errors much efficiently but we care about readability more and worrying about how efficiently code is going to error is completely extraneous as our code is going to stop operating anyways.

Abstract erroring

Sometimes we want a more efficient and readable way to handle erroring and safety checks. We can apply the concept of unit-testing and create readable tests like below:

local function ...(Argument)
Argument = Check(Argument).Type.ShouldBe("string") and Argument or error("") -- We could also error internally and abstract our error message
end

Although preferably when we are doing tight performance operations unit-test patterns can become a problem however Luau is constantly optimizing.

Native vs Luau Consistency

type() should only be used to check native types for consistency and context reasons while typeof() should only be preserved for Luau types.

Metadata

Metadata should always be abstracted into modules.

Format and Structure

Your Metadata module should return an anonymous table unless needed. Here's an example below:

return { -- Metadata modules must only return an anonymous table

    TropicalFish = { 
        LevelToEarn = 1,        -- What level this is earned at
        MaxHunger = 18,         -- Max hunger
        SpeedBoast = 5,         -- How fast the fish can travel when boosting
        SpeedBoostDuration = 2, -- Speed boost duration in seconds
        BiteDamage = 1,         -- The damage of the bite
        Size = 1,               -- Size of fish, serves to who can eat who
        Speed = 1,              -- Speed at which the fish swims at
        Model = nil,            -- Template Model 
        Species = "SmallFish",  -- Class
 
        Animations = {          -- Animation set
            Swim = "",
            MouthOpen = ""
        }
    }
 
}

Object-oriented

Meta-Mechanism

Metatables should only be used for object-oriented patterns. Internally, Lua’s meta-mechanisms are very inefficient as numerous register accesses have to be made and elements have to be copied and moved continuously. Do not worry, however, as Luau plans on implementing a built-in inheritance syntax in the near future at the time of this writing.

Classes

Your index metamethod should point directly back at the table for Luau optimizations to be made and for fast method calls to be properly made [1]. Classes should be formatted just like below:

local FishClass = {}
FishClass.__index = FishClass

Constructor

You’re constructor must be constructed and accessed using the following notation below:

function Class.new()

Object.new()

Your constructor may be PascalCase or camelCase, this one is up to you as people commonly use camelCase for their constructors. Methods must use the notation: Object:Hello()

Private Fields

You should denote private fields with the convention: _ before the variable or function like so:

function Class:_InternalFunction()

end

Format and Structure

Here is an example of how everything should go together:

local HumanoidClass = {}                -- Must include "Class" here
HumanoidClass.__index = HumanoidClass   -- Declare the metamethod here and not in the table unless needed
 
 
function HumanoidClass.new(PlayerName, ...) -- .new( PlayersName: string, ExtraAttributes: ... )
    PlayerName = PlayerName or error("Function takes the players name as its 1st argument");
    
    return setmetatable({
        _WalkSpeed = 0,
        _Sitting = false,
        _PlayersName = PlayerName,     -- Put _ at the beginning  of the variable to show it's a private variable.
        _ExtraAttributes = ...
    }, HumanoidClass)
end 
 
 
function HumanoidClass:Walk(WalkSpeed) -- :Walk( Speed: number )
    WalkSpeed = WalkSpeed or 16;
    
    self._WalkSpeed = WalkSpeed
    
    -- Walk
end
 
 
function HumanoidClass:ToggleSitting(ToggleSittingDuration) -- :Sit( ToggleSittingDuration: number )
    local HumanoidSitting = self._Sitting
    HumanoidSitting = not HumanoidSitting
    
    if ToggleSittingDuration then
        wait(SittingDuration)
        HumanoidSitting = not HumanoidSitting
    end
    
end
 
return HumanoidClass

References

[1] https://roblox.github.io/luau/performance

[2] G. Ann Campbell. 2018. Cognitive Complexity - A new way of measuring understandability. Technical Report. SonarSource SA, Switzerland. https://www.sonarsource.com/docs/CognitiveComplexity.pdf

[3] Martin, Robert C. Clean Code: A Handbook of Agile Software Craftsmanship. Upper Saddle River, NJ: Prentice Hall, 2009. Print.

Tags: Luau, Lua, StyleGuide