Roblox Lua Style guide (original) (raw)

This style guide aims to unify as much Lua code at Roblox as possible under the same style and conventions.

This guide is designed after Google's C++ Style Guide. Although Lua is a significantly different language, the guide's principles still hold.

Guiding Principles

File Structure

Files should consist of these things (if present) in order:

  1. An optional block comment talking about why this file exists
    • Don't attach the file name, author, or date -- these are things that our version control system can tell us.
  2. Services used by the file, using GetService
  3. Module imports, using require
  4. Module-level constants
  5. Module-level variables and functions
  6. The object the module returns
  7. A return statement!

Requires

General

Requires Structure

In order to maintain readability, require statements can be grouped into blocks. Require blocks should mimic the project's internal structure and consist of these things (if present) in order:

  1. A definition of a common ancestor.
  2. A block of all imported packages.
  3. A block for definitions derived from packages, which may be broken down recursively by subfolder.
  4. A block for modules imported from the same project, which may be broken down recursively by subfolder.

If blocks of requires are used: * Blocks should be sorted alphabetically, first by the name of the subfolder which groups the block, and then by module name. * If multiple require statements share a common path, prefer to move those to a separate block.

Requiring Libraries

Libraries are projects which define an API for external consumers to use, typically by providing a top-level table which requires other modules. Libraries will typically provide a structured public API composed from internal modules. This allows libraries to have stable interfaces even when internal details may change, and can be used both for sharing code as well as for organizing one's own code.

Example

For a project that looks like the following:

MyProject |- FooBar | |- Foo.lua | |- Bar.lua |- MyClass.lua |- Packages | |- Baz.lua | | |- Bazifyer.lua | | |- UnBazifyer.lua

MyClass should define the following import block:

-- 1. A definition of a common ancestor. -- Use a relative path to make sure your project works in multiple locations! local MyProject = script.Parent

-- 2. A block of all imported packages. -- Baz is a library we depend on in our project, so we require its API directly... local Baz = require(MyProject.Packages.Baz)

-- 3. A block for definitions derived from packages. -- ...and then access its members through that API. These are simple so we don't need to break them down. local Bazifyer = Baz.Bazifyer local UnBazifyer = Baz.UnBazifyer

-- 4. A block for modules imported from the same project. -- Defining the path to FooBar separately makes it faster to write and for others to read! local FooBar = MyProject.FooBar local Foo = require(FooBar.Foo) local Bar = require(Foobar.Bar)

Metatables are an incredibly powerful Lua feature that can be used to overload operators, implement prototypical inheritance, and tinker with limited object lifecycle.

At Roblox, we limit use of metatables to a couple cases:

Prototype-based classes

There are multiple ways of defining classes in Lua. The method described below is recommended because it takes advantage of Luau's typing system. Providing a strongly-typed class definition helps developers use and improve your class by documenting its expected use, and allowing analysis tools and IDEs to warn against possible bugs when inconsistencies are detected.

First up, we create a regular, empty table:

Next, we assign the __index member on the class back to itself. This is a handy trick that lets us use the class's table as the metatable for instances as well.

When we construct an instance, we'll tell Lua to use our __index value to find values that are missing in our instances. It's sort of like prototype in JavaScript, if you're familiar.

MyClass.__index = MyClass

In order to support strict type inference we are describing the shape of our class. This introduces some redundancy as we specify class members twice (once in the type definition, once as we build the actual instance), but warnings will be flagged if the two definitions fall out of sync with each other.

-- Export the type if you'd like to use it outside this module export type ClassType = typeof(setmetatable( {} :: { property: number, }, MyClass ))

Next, we create a default constructor for our class and assign the type definition from above to its return value (self).

-- The default constructor for our class is called new by convention. function MyClass.new(property: number): ClassType local self = { -- Define members of the instance here, even if they're nil by default. property = property, }

-- Tell Lua to fall back to looking in MyClass.__index for missing fields.
setmetatable(self, MyClass)

return self

end

We can also define methods that operate on instances. Prior to Luau's type analysis capabilities, popular convention has suggested using a colon (:) for methods. But in order to help the type checker understand that self has type ClassType, we use the dot (.) style of definition which allows us to specify the type of self explicitly. These methods can still be invoked on the resulting instances with a colon as expected.

In the future, Luau will be able to understand the intended type of self without any extra type annotations.

function MyClass.addOne(self: ClassType) self.property += 1 end

At this point, our class is ready to use!

We can construct instances and start tinkering with it:

local instance = MyClass.new(0)

-- Properties on the instance are visible, since it's just a table: print(tostring(instance.property)) -- "0"

-- Methods are pulled from MyClass because of our metatable: instance:addOne() print(tostring(instance.property)) -- "1"

Further additions you can make to your class as needed:

Guarding against typos

Indexing into a table in Lua gives you nil if the key isn't present, which can cause errors that are difficult to trace!

Our other 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:

local MyEnum = { A = "A", B = "B", C = "C", }

setmetatable(MyEnum, { __index = function(self, key) error(string.format("%q 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 engineers track down bugs more easily.

General Punctuation

General Whitespace

Newlines in Long Expressions

In some situations where we only ever expect table literals, the following is acceptable, though there's a chance automated tooling could change this later. In particular, this comes up a lot in Roact code (doSomething being Roact.createElement).

local aTable = {{
    aLongKey = aLongValue,
    anotherLongKey = anotherLongValue,
}, {
    aLongKey = anotherLongValue,
    anotherLongKey = aLongValue,
}}

doSomething({
    aLongKey = aLongValue,
    anotherLongKey = anotherLongValue,
}, {
    aLongKey = anotherLongValue,
    anotherLongKey = aLongValue,
})

However, this case is less acceptable if there are any non-tables added to the mix. In this case, you should use the style above.

doSomething({
    aLongKey = aLongValue,
    anotherLongKey = anotherLongValue
}, notATableLiteral, {
    aLongKey = anotherLongValue,
    anotherLongKey = aLongValue
})

doSomething(
    {
        aLongKey = aLongValue,
        anotherLongKey = anotherLongValue
    },
    notATableLiteral,
    {
        aLongKey = anotherLongValue,
        anotherLongKey = aLongValue
    }
)

end

if-then-else expressions

Good:
local scale
if
someReallyLongFlagName()
or someOtherReallyLongFlagName()
then
scale = Vector2.new(1, 1) + someVectorOffset
+ someOtherVector
else
scale = Vector2.new(1, 1) + someNewVectorOffset
+ someNewOtherVector
end

Blocks

Literals

Tables

Functions

Naming

FooThing.lua:

local FOO_THRESHOLD = 6

local FooThing = {}

FooThing.someMemberConstant = 5

function FooThing.go() print("Foo Delta:", FooThing.someMemberConstant - FOO_THRESHOLD) end

return FooThing

Yielding

Do not call yielding functions on the main task. Wrap them in coroutine.wrap or delay, and consider exposing a Promise or Promise-like async interface for your own functions.

Pros:

Cons:

Error Handling

When writing functions that can fail, return success, result, use a Result type, or use an async primitive that encodes failure, like Promise.

Do not throw errors except when validating correct usage of a function.

local function thisCanFail(someValue) assert(typeof(someValue) == "string", "someValue must be a string!")

if success() then
    return true, "Congratulations! You won!"
else
    return false, Error.new("ERR_BLAH", "Something horrible failed!")
end

end

Pros:

Cons:

Exceptions:

General Roblox Best Pratices