These guidelines should be followed for all Lua code inside of Fusion.
The rules and guidelines set out here are derived from Roblox's style guide, which can be found here: https://roblox.github.io/lua-style-guide/
-
The purpose of this style guide is to avoid arguments.
- There's no one right answer to how to format code, but consistency is important. We agree to accept this one, somewhat arbitrary standard so we can spend more time writing code and less time arguing about formatting details.
-
Optimise code for reading, not writing.
- You will write your code once. However, it will be read many times by many people, likely including yourself long after you've forgotten how it works.
- For this reason, it's important to streamline figuring out how the code works, since you will have to do this many times.
- All else being equal, consider what the diffs might look like. It's much easier to read a diff that doesn't involve moving things between lines. Clean diffs make it much easier to review code.
-
Avoid magic, such as surprising or dangerous Lua features.
- Magical code is really nice to use, until something goes wrong. Then nobody knows why it's broken or how to fix it.
- Metatables and
getfenv
/setfenv
are examples of powerful features that should be used with care.
-
Be consistent with idiomatic Lua when appropriate.
- Scripts should be grouped into folders, to make it easier to navigate the codebase.
All scripts should consist of these things (if present) in order:
- A block comment talking about why this file exists, or documenting it's
functionality.
- Don't add the file name, author or date - those are things that our version control can tell us.
- Services used by the file, using
GetService
.- This includes services such as
Workspace
orLighting
- consistency is important!
- This includes services such as
- Imported modules, using
require
.- Modules from the same folder or location should stay next to each other.
- Script-level constants.
- Script-level variables and functions.
- (for ModuleScripts) The object returned by the module.
- (for ModuleScripts) The return statement.
require
calls should be at the top of the file, making dependencies static.- If there's an issue with two modules requiring each other cyclically, the structure of that code needs to be reconsidered.
- When requiring a module inside Fusion's source code, use relative paths. To
keep these paths clean, use a variable called
Package
to store where the root of the library is.- This makes it clear where to find the source of the module within the source code.
local Package = script.Parent.Parent local Foo = require(Package.Utils.Foo) local Bar = require(Package.Reactive.Bar)
- Elsewhere, prefer absolute paths when requiring modules:
local ReplicatedStorage = game:GetService("ReplicatedStorage") local Foo = require(ReplicatedStorage.Foo)
Metatables are an incredibly powerful Lua feature that can be ued to overload operators, implement prototypical inheritance, and tinker with limited object lifecycle. However, they can also cause unexpected or surprising behaviour, and so they should be used very sparingly, and only if you know what you're doing.
When using metatables, they should be sufficiently documented, normally by adding comments explaining the purpose and intended function of a metatable.
Some common uses of metatables are described below. Keep in mind that Fusion often has ready-to-go implementations of many of these; those should always be preferred over manual implementations.
Metatables are commonly used to implement prototype-based classes in Lua. Fusion implements classes in a way which is designed to avoid cyclic metatables, by separating the constructor from class methods.
Start by creating a blank table, conventionally called class
:
local class = {}
We can then define a constructor function for creating new objects of the
class. To create a new object of the class, create a new table, and apply a
metatable with __index
set to class
. That way, when indexing the
object, it'll fall back to the class
table if no member was found.
Since this constructor is normally used as the return value of the module it's in, it should adopt the module's name, as dictated by the naming conventions:
local function MyClass()
-- create a table to serve as our object, and use a metatable to fall back
-- to `class` for missing fields.
local self = setmetatable({}, {__index = class})
-- define some members here
self.phrase = "bark"
-- return the object
return self
end
We can define methods that operate on objects, using a colon (:
) to take
advantage of Lua's syntactic sugar for objects:
-- equivalent to: `function class.bark(self)`
function class:bark()
print("My phrase is", self.phrase)
end
At this point, our class is ready to use! We can construct new objects and start tinkering with it:
local myObject = MyClass()
-- object members are visible, since it's just a table:
print(myObject.phrase) --> bark
-- methods are pulled from `object` because of our metatable:
myObject:bark() --> My phrase is bark
Some further additions you can make to your class as needed:
- Introduce a
__tostring
metamethod to make debugging easier - Add a
type
string to objects of your class - this is used to differentiate objects of different class types
Indexing into a table in Lua gives you nil
if the key isn't present, which can
cause errors that are difficult to trace!
Another major use case for metatables is to prevent certain forms of this
problem. For types that act like enums, we can carefully apply an __index
metamethod that throws an error when an invalid member is accessed:
local MyEnum = {
A = "A",
B = "B",
C = "C"
}
setmetatable(myEnum, {
__index = function(self, key)
error(string.format("%s is not a valid member of MyEnum",
tostring(key)), 2)
end
})
Since __index
is only called when a key is missing in the table, MyEnum.A
and MyEnum.B
will still give you back the expected values, but MyEnum.FROB
will throw, hopefully helping contributors track down bugs more easily.
As a safety measure, it's often desirable to 'lock' tables, so they can't be written to. This is usually done with enums or public API members.
To prevent scripts from writing to new indexes, we can apply the __newindex
metamethod to the table:
local myTable = {
foo = "2",
bar = true
}
setmetatable(myTable, {
__newindex = function(self, key, value)
error("myTable is not writable", 2)
end
})
Note that, because __newindex
only fires when writing to a nil
index, this
won't prevent writes to indexes with non-nil
values. Also, keep in mind that
assigning nil
to an index after adding the metamethod makes that index
read-only.
- Don't use semicolons (
;
). They are generally only useful to separate multiple statements on a single line, but you shouldn't be putting multiple statements on a single line anyway.- This includes using semicolons in tables; prefer to use commas (
,
) to delimit values in tables.
- This includes using semicolons in tables; prefer to use commas (
- In comments and other documentation, use backticks when referencing code,
and indentation when embedding longer blocks of code
-- `foo` will be set to `5 + os.clock()` local foo = 5 + os.clock() --[[ An example implementation of a function using `os.clock()`: local function getMinutes() local now = os.clock() return math.floor(now / 60) end The above code returns the integer number of minutes since the epoch. ]]
- Indent with tabs, not spaces.
- Tabs use less characters than spaces do, and allows contributors to change the tab size to their preference in their editor of choice.
- Keep lines under 120 columns wide, assuming 4-wide tabs.
- Wrap comments to 80 columns wide, assuming 4-wide tabs.
- This is different from normal code; the hope is that short lines help to improve readability of comment prose, but is too restrictive for code.
- Don't leave whitespace at the end of lines.
- If your editor has an auto-trimming function, turn it on!
- Add a newline at the end of the file.
- Don't align code vertically; it makes code more difficult to edit and often
gets messed up by subsequent editors.
-- Good: local frobulator = 12345 local grog = 17 -- Bad: local frobulator = 12345 local grog = 17
- Use a single empty line to express groups when useful. Don't start blocks with
a blank line. Excess empty lines harm the readability of the whole file.
local Foo = require(Package.Utils.Foo) local function gargle() -- gargle gargle end Foo.frobulate() Foo.frobulate() Foo.munge()
- Use one statement per line. Put function bodies on new lines.
- This is especially true for functions that return multiple values. It's much easier to spot mistakes (and harder to make the mistake in the first place) if the function isn't on one line.
- This is also true for
if
blocks, even if their body is just a return statement. - This also helps code diff better.
-- Good: table.sort(stuff, function(a, b) local sum = a + b return math.abs(sum) > 2 end) -- Bad: table.sort(stuff, function(a, b) local sum = a + b return math.abs(sum) > 2 end)
- Put a space between operators, except when clarifying precedence.
-- Good: print(5 + 5 * 6^2) -- Bad: print(5+5* 6 ^2)
- Put a space after commas in tables and function calls.
-- Good: local friends = {"bob", "amy", "joe"} foo(5, 6, 7) -- Bad: local friends = {"bob","amy" ,"joe"} foo(5,6 ,7)
- When creating blocks, inline any opening syntax elements.
-- Good: local foo = { bar = 2, } if foo then -- do something end -- Bad: local foo = { bar = 2, } if foo then -- do something end
- Avoid putting curly braces for tables on their own line. Doing so harms
readability, since it forces the reader to move to another line in an awkward
spot in the statement.
-- Good: local foo = { bar = { baz = "baz", }, } frob({ x = 1, }) -- Bad: local foo = { bar = { baz = "baz", }, } frob( { x = 1, }) -- Exception: -- In function calls with large inline tables or functions, sometimes it's -- more clear to put braces and functions on new lines: foo( { type = "foo", }, function(something) print("Hello," something) end ) -- As opposed to: foo({ type = "foo", }, function(something) -- How do we indent this line? print("Hello,", something) end)
-
First, try and break up the expression so that no one part is long enough to need newlines. This isn't always the right answer, as keeping an expression together is sometimes more readable than trying to parse how several small expressions relate, but it's worth pausing to consider which case you're in.
-
It is often worth breaking up tables and arrays with more than two or three keys, or with nested sub-tables, even if it doesn't exceed the line length limit. Shorter, simpler tables can stay on one line though.
-
Prefer adding the extra trailing comma to the elements within a multiline table or array. This makes it easier to add new items or rearrange existing items.
-
Break dictionary-like tables with more than a couple keys onto multiple lines.
-- Good: local foo = {type = "foo"} local bar = { type = "bar", phrase = "hooray", } -- It's also okay to use multiple lines for a single field local baz = { type = "baz", } -- Bad: local stuff = {hello = "world", hola = "mundo", howdy = "y'all", sup = "homies"}
-
Break list-like tables onto multiple lines however it makes sense.
- Make sure to follow the line length limit!
local libs = {"fusion", "luau", "class", "maid", "event"} -- You can break these onto multiple lines, which makes diffs cleaner: local libs = { "fusion", "luau", "class", "maid", "event", } -- We can also group them, if grouping has useful information: local libs = { "fusion", "luau", "class", "maid", "event" }
-
For long argument lists or longer, nested tables, prefer to expand all the subtables. This makes for the cleanest diffs as further changes are made.
local aTable = { { aLongKey = aLongValue, anotherLongKey = anotherLongValue, }, { aLongKey = anotherLongValue, anotherLongKey = aLongValue, }, } doSomething( { aLongKey = aLongValue, anotherLongKey = anotherLongValue, }, { aLongKey = anotherLongValue, anotherLongKey = aLongValue, } )
-
For long expressions try and add newlines between logical subunits. If you're adding up lots of terms, place each term on its own line. If you have parenthesized subexpressions, put each subexpression on a newline.
-
Place the operator at the beginning of the new line. This makes it clearer at a glance that this is a continuation of the previous line.
-
If you have to need to add newlines within a parenthesized subexpression, reconsider if you can't use temporary variables. If you still can't, add a new level of indentation for the parts of the statement inside the open parentheses much like you would with nested tables.
-
Don't put extra parentheses around the whole expression. This is necessary in Python, but Lua doesn't need anything special to indicate multiline expressions.
-
-
For long conditions in
if
statements, put the condition in its own indented section and place thethen
on its own line to separate the condition from the body of theif
block. Break up the condition as any other long expression.-- Good: if someReallyLongCondition and someOtherReallyLongCondition and somethingElse then doSomething() doSomethingElse() end -- Bad: if someReallyLongCondition and someOtherReallyLongCondition and somethingElse then doSomething() doSomethingElse() end if someReallyLongCondition and someOtherReallyLongCondition and somethingElse then doSomething() doSomethingElse() end if someReallyLongCondition and someOtherReallyLongCondition and somethingElse then doSomething() doSomethingElse() end
- Don't use parentheses around the conditions in
if
,while
, orrepeat
blocks. They're not necessary in Lua!if condition then -- ... end while condition do -- ... end repeat -- ... until condition
- Use
do
blocks if limiting the scope of a variable is useful.local getId do local lastId = 0 getId = function() lastId += 1 return lastId end end
- Use double quotes when declaring single-line string literals.
- Using single quotes means we have to escape apostrophes, which are often useful in English words.
- Empty strings are easier to identify with double quotes, because in some fonts two single quotes might look like a single double quote.
- Use brackets (
[[
and]]
) when declaring multi-line string literals.- Don't indent multi-line string literals, as the tab characters will be interpreted as part of the string!
print("Here's a message") print([[ This is a longer message, which is designed to span multiple lines to demonstrate how multi-line strings work. ]])
- Avoid tables with bost list-like and dictionary-like keys.
- Iterating over these mixed tables is troublesome.
- If you do end up using mixed tables, make sure to 'unmix' the array part from the dictionary part as soon as possible to minimise possible bugs.
- Iterate over list-like tables with
ipairs
and dictionary-like tables withpairs
.- This helps clarify what kind of table we're expecting in a given block of code!
- Keep the number of arguments to a given function small, preferably 1 or 2.
- Similarly, keep the number or returned values from a function small.
- When calling a function, you may only omit parentheses when passing in a
dictionary-like table.
- Omitting parentheses when passing in an array-like table can cause some confusion, as the curly braces may look like parentheses at a glance, hiding that a table is being passed.
- This isn't a problem with dictionary-like tables. In fact, this is a common pattern in Fusion, where constructors for primitives are often called this way.
-- Good: local x = doSomething("home") local y = doSomething({1, 2, 3, 4, 5}) local z = doSomething({foo = 2, bar = "frob"}) -- omitting parentheses is only OK for dictionary-like tables local w = doSomething { foo = 2, bar = "frob" } -- Bad: local x = doSomething "home" local y = doSomething {1, 2, 3, 4, 5}
- Declare named functions using function-prefix syntax. Non-member functions
should always be local.
- An exception can be made for late-initializing functions, for example in conditionals.
-- Good: local function add(a, b) return a + b end -- Bad: function add(a, b) return a + b end local add = function(a, b) return a + b end -- Exception: local doSomething if CONDITION then function doSomething() -- Version of doSomething with CONDITION enabled end else function doSomething() -- Version of doSomething with CONDITION disabled end end
- When declaring a function inside a table, use function-prefix syntax. Use
a dot (
.
) or colon (:
) to denote intended calling convention.-- Good: -- This function should be called as Frobulator.new() function Frobulator.new() return {} end -- This function should be called as Frobulator:frob() function Frobulator:frob() print("Frobbing", self) end -- Bad: function Frobulator.garb(self) print("Frobbing", self) end Frobulator.jarp = function() return {} end
- Wrap comments to 80 columns wide.
- It's easier to read comments with shorter lines, but fitting code into 80 columns can be challenging.
- Use single line comments for inline notes.
- If the comment spans multiple lines, use multiple single line comments.
-- This condition is really important because the world would blow up if it -- were missing. if not foo then stopWorldFromBlowingUp() end
- Use block comments for documenting items:
- Use a block comment at the top of files to describe their purpose.
- Use a block comment before functions or objects to describe their intent.
--[[ Shuts off the cosmic moon ray immediately. Should only be called within 15 minutes of midnight Mountain Standard Time, or the cosmic moon ray may be damaged. ]] local function stopCosmicMoonRay() end
- Comments should focus on why code is written a certain way, instead of
what the code is doing.
-- Good: -- Without this condition, the aircraft hangar would fill up with water. if waterLevelTooHigh() then drainHangar() end -- Bad: -- Check if the water level is too high. if waterLevelTooHigh() then -- Drain the hangar drainHangar() end
- No section comments.
- Comments that only exist to break up a large file are a code smell; you probably need to find some way to make your file smaller instead of working around that problem with section comments.
- Comments that only exist to demark already obvious groupings of code
(e.g.
--- VARIABLES ---
) and overly stylized comments can actually make the code harder to read, not easier. - Additionally, when writing section headers, you (and anyone else editing the file later) have to be thorough to avoid confusing the reader with questions of where sections end.
- Some examples of other ways of breaking up files:
- Move inner classes and static functions into their own files, which aren't included in the public API. This also makes testing those classes and functions easier.
- Check if there are any existing libraries that can simplify your code. If you're writing something and think that you could make part of this into a library, there's a good chance someone already has.
- If you can't break the file up, and still feel like you need section
headings, consider these alternatives:
- If you want to put a section header on a group of functions, put that
information in a block comment attached to the first function in that
section. You should still make sure the comment is about the function
its attached to, but it can also include information about the section
as a whole. Try and write the comment in a way that makes it clear
what's included in the section.
--[[ All of the readX functions return the next token from the string passed in to the Reader or returns nil if the next token doesn't match the type the function is trying to read. local test = "123 ABC" i = reader:readInt() print(i, ",", test.remaining) -- 123 , ABC readInt reads an integer, positive or negative. ]] function Reader:readInt() -- ... -- readFloat reads a floating point number, but does not accept -- scientific notation function Reader:readFloat() -- ...
- The same can be done for a group of variables in some cases. All the same caveats apply though, and you have to consider whether one block comment or a normal comment on each variable (or even using just whitespace to separate groups) would be more readable.
- General organization of your code can aid readibility while making logical sections more obvious as well. Module level variables and functions can appear in any order, so you can sometimes put a group of variables above a group of functions to make a section.
- If you want to put a section header on a group of functions, put that
information in a block comment attached to the first function in that
section. You should still make sure the comment is about the function
its attached to, but it can also include information about the section
as a whole. Try and write the comment in a way that makes it clear
what's included in the section.
- Spell out words fully! Abbreviations generally make code easier to write, but
harder to read.
- Make sure that names don't get too long, however! Extremely long names can also be detrimental to code readability.
- Avoid single-letter names; names should be adequately descriptive.
- An exceptions to this rule are coordinates, e.g.
x
,y
andz
- In code working with multiple coordinate spaces (e.g. object and world
space), or a combination of Offset and Scale (for UDims), this is less
acceptable. Prefer prefixed names in those cases, for example
objectX
andworldY
.
- In code working with multiple coordinate spaces (e.g. object and world
space), or a combination of Offset and Scale (for UDims), this is less
acceptable. Prefer prefixed names in those cases, for example
- Another exception is generics in typed Luau, e.g.
type Foo<T> = () -> T
-- Good: local function isFrob(value) return tostring(value) == "frob" end for index, value in pairs(garb) do print(index, "=", value) end -- Bad: local function isFrob(x) return tostring(x) == "frob" end for i, v in pairs(garb) do print(i, "=", v) end
- An exceptions to this rule are coordinates, e.g.
- Use
PascalCase
for all Roblox APIs.camelCase
APIs are mostly deprecated, but still work for now.
- Use
PascalCase
for enum-like objects. - Use
PascalCase
for named Luau type definitions. - Use
PascalCase
for functions that construct class objects - this is in line with how classes are conventionally named:-- Good: local foo = State(5) local bar = Maid() -- Bad: local foo = state(5) local bar = maid()
- Use
camelCase
for variables, member values and functions. - Use
LOUD_SNAKE_CASE
for constants. - For acronyms within names, don't capitalise the whole thing. For example,
aJsonVariable
orMakeHttpCall
.- The exception to this is when the abbreviation represents a set. For
example, in
myRGBValue
orGetXYZ
. In those cases,RGB
should be treated as an abbreviation of 'Red Green Blue' and not as an acronym.
- The exception to this is when the abbreviation represents a set. For
example, in
- If a member of a class is private, prefix it with one underscore, for example
_foob
.- Lua does not have visibility rules, but using underscores helps make private access stand out.
- A file's name should match the name of whatever it exports.
- If your module exports a single function named
doSomething
, the file should be nameddoSomething.lua
.
- If your module exports a single function named
Don't call yielding functions on the main thread. Wrap them in coroutine.wrap
or delay
, and consider exposing a Promise or Promise-like async interface for
your own functions.
Unintended yielding can cause hard-to-track data races. Simple code involving callbacks can cause confusing bugs if the input callback yields:
local value = 0
local function doSomething(callback)
local newValue = value + 1
callback(newValue)
value = newValue
end
Similarly, if a callback is not allowed to yield, your code should check that it doesn't yield, so the error can be caught early on. Fusion provides utility functions to assert a callback doesn't yield while it's running.
When writing functions that are expected to fail sometimes, return
success, result
, use a Result
type, or use an async primitive that encodes
failure, like Promise
.
Avoid throwing errors unless your code encounters something that might be a bug - errors are not encoded into a function's contract explicitly, so your caller isn't forced to consider whether an error will happen, and how any errors should be dealt with.
-- Good:
-- type checking should throw an error, since incorrect types is likely a bug
assert(typeof(number) == "number", "Must pass number to function")
if foo < 0 then
error("foo must not be negative")
end
-- Bad:
-- a player running out of money is not typically a bug, so this would be better
-- implemented using `success, result` or similar
if numCoins < itemPrice then
error("Player doesn't have enough coins for transaction")
end
When calling functions that communicate failure by throwing, wrap calls in
pcall
and make it clear via comment what kinds of errors you're expecting to
handle.
Except for debugging, any code in Fusion that throws an error, emits a warning or prints a message should do so using Fusion's logging utilities. These logging utilities add extra information to the message, so the user of the library can easily find more information about where they're coming from.
These logging utilities work with message IDs rather than plain text; if you need to log a new kind of message, add it to the list of messages under a new message ID.
Generally, you should avoid reusing message IDs that are used in other areas of the library. Message IDs should be limited to a small area, so the documentation for them can provide more specific details about what's going on, which helps debugging efforts.
When adding a new message to the list of messages, make sure to document the new message in Fusion's 'Errors & Messages' section of the API Reference.
- All services should be referenced using
GetService
at the top of the file. - When importing a module, use the name of the module for its variable name.