Skip to content

Instantly share code, notes, and snippets.

@catwell
Last active August 21, 2022 15:24
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save catwell/b3c01dbea413aa78675740546dfd5ce2 to your computer and use it in GitHub Desktop.
Save catwell/b3c01dbea413aa78675740546dfd5ce2 to your computer and use it in GitHub Desktop.
Lima Lua style

Lima Lua style guide

About this style guide

How to read this style guide

Refer to RFC 2119 to interpret MUST, SHOULD and MAY.

Inspiration

Non-cosmetic

Ternaries

Ternaries with litterals are OK:

local x = y and "yes" or "no"

Ternaries with a variable as the second element should be avoided. Remember:

1 and false or 2 -- do not do this, evaluates to 2

Modules

Modules MUST NOT set or access any global variable when required.

Modules MUST always return a table when required. If you would rather like to return a function you MAY set the __call metamethod on the table instead.

Internal modules that can be used in different contexts MAY use the lima namespace. Modules that are released as Open Source SHOULD NOT be namespaced.

When a module behaves like a class, it SHOULD expose a constructor called new.

local dog = require "lima.example.dog"
local my_dog = dog.new()

A module SHOULD NOT have state. If a module needs configuration, turn it into a factory. For instance, this (from an Open Source module we did not write):

local mp = require "MessagePack"
mp.set_integer("unsigned")

should have this interface instead:

local messagepack = require "MessagePack"
local mp = messagepack.new({integer = "unsigned"})

luacheck

Your code MUST pass luacheck. If it does not with default settings, you must provide a .luacheckrc with sensible exceptions.

errors

Functions that can fail for reasons that are expected (e.g. I/O) SHOULD return nil and a (string) error message on error, possibly followed by other return values such as an error code, like the Lua io library, LuaSocket and luaposix do.

On errors such as API misuse, an error SHOULD be thrown, either with error() or assert().

A different error handling convention MAY be used in parts of the code where it makes sense, for instance in FUSE code where FLU expects you to throw POSIX error codes.

preconditions

You SHOULD verify the type and shape of arguments passed to public functions in your modules using assert. You MAY also do so for private functions. You MAY avoid those checks in functions that are intended to be very fast.

local function is_posint(x)
    return (math.floor(x) == x) and (x >= 0)
end

local function foo(s, t)
    assert(type(s) == "string")
    t = t or {}
    t.x = t.x or 42
    assert(
        type(t) == "table" and
        is_posint(t.x)
    )
end

Cosmetic

Homogeneity

Style homogeneity within a file trumps everything else, including all the rules below.

If you import open source code and modify it slightly, you MUST stick to the style of the original code. If you modify it a lot and do not intend to merge any upstream changes ever, you MAY convert the code to Lima style. You MUST do this in a separate commit which does not include any other modification. You SHOULD use "cosmetics" as the commit message.

Tests and other DSLs

Tests can be considered as a DSL, and as such are not required to follow this style guide strictly. It MAY make sense to have different rules in a DSL if they improve legibility.

Whitespace

All new code MUST be indented with four spaces.

Most operators SHOULD be surrounded with spaces. Commas MUST have no whitespace before them and some after them.

local x, y = 1, 2

You MUST NOT leave a space between the name of a function and the opening parenthesis of the arguments (despite the fact that the Lua manual does this).

You MUST leave a space between a function and its litteral string or table argument when called with no parentheses.

You SHOULD NOT use whitespace to align things vertically.

-- Do not do this.
local chunky = 4
local foo    = 65

You SHOULD keep line length below 80 characters. When needed, you SHOULD indent tables and function arguments like this:

some_function(
    argument_1,
    argument_2,
    a_very_long_argument,
    other_arguments...
)

Alternatively, you MAY put several arguments on the same line, but you SHOULD always indent with four spaces and leave the closing parenthesis on its own line.

some_function(
    argument_1, argument_2, a_very_long_argument,
    other_arguments...
)

Defining and calling functions

In Lua, function f(...) is syntactic sugar for f = function(...) and local function f(...) is syntactic sugar for local f; f = function(...).

You SHOULD use this sugar only in its second form, when declaring a new local function.

You MUST NOT use the sugar:

  • to define global functions;
  • to define functions in tables;
  • to define a function whose local is already declared.
local g
local function f(...) end
g = function(...) end
local t = {
    f = f,
    h = function(...) end,
}
t.g = g
t.i = function(...) end

In Lua, x:f(...) is syntactic sugar for x.f(x, ...).

You MUST NOT use the colon sugar for method definition.

local t = {n = 42}
t.f = function(self) print(self.n) end -- OK
function t:f() print(self.n) end -- not OK

You MUST use the sugar for method calls.

t:f() -- OK
t.f(t) -- not OK

Note that this is completely different from using this in a module:

local function f(self) ... end

local g = function(self)
    f(self) -- and not self:f()
end

The above does not have the same meaning (it creates a closure, meaning that the user cannot override the definition of f as called in g). This pattern is perfectly OK. What would not be OK would be self.f(self).

Also, of course when using pcall you don't have a choice:

pcall(self.f, self)

Remember: try to keep a homogeneous style within a single file or module.

Function calls

You SHOULD NOT omit parenthesis for functions that take a unique string litteral argument. An exception to this rule is require, which you SHOULD use like this.

You SHOULD NOT omit parenthesis for functions that take a unique table argument on a single line. You MAY do so for table arguments that span several lines.

local a_module = require "a_module"
local an_instance = a_module.new {
    a_parameter = 42,
    another_parameter = "yay",
}
an_instance:a_function("a_string")

Semicolons and commas

In general you SHOULD NOT use semicolons. In particular, the separator between table elements MUST always be a comma.

If you write a table on multiple lines, you SHOULD add a comma after the last element. If you write a table on a single line, you SHOULD NOT.

local t = {
    x = 1,
    y = 2,
}

local t = {x = 1, y = 2}

Variable names

You SHOULD use short variable names (even single letters) for variables with limited scope and slightly longer variable names for functions parameters and for variables that may be used far from their declaration (but you SHOULD NOT have many such variables).

You SHOULD use short names for functions and methods intended to be used frequently and longer, more descriptive names for functions and methods that will be used more rarely.

In general, you SHOULD use snake_case for locals, functions, methods, table fields and module names. You SHOULD use UPPER_CASE for constants.

You SHOULD NOT use CamelCase and you MUST NOT use snakeCase.

Anonymous functions

You SHOULD NOT use anonymous functions if they span several lines.

You MAY do so if they fit on a single line.

local function my_handler(...)
    --[[ do something long ]]
end

local t = {
    handler_1 = my_handler,
    handler_2 = function(...) end,
}

rpc_call("SOME_RPC", my_handler)
rpc_call("SOME_RPC", function(...) print("RPC called") end)

Object @ Lima

Template

The code is split and ordered in the following parts:

Header

  1. Create an empty table M which will contain the methods of the class.
  2. Create the metatable MT.
  3. Create the table representing the class. We call it Class in this example, but it should be named like the class.
local M = {}
local MT = { __index = M }
local Class = { methods = M }

Methods

A public method foo is defined like this:

--- Documentation about `foo` in LDoc format.
M.foo = function(self, ...)
    -- do things
end

A private method bar is defined like this:

-- Optional documentation about `bar`.
local function bar(self, ...)
    -- do things
end

A metamethod qux is defined like this:

--- Documentation about `qux` in LDoc format.
MT.__qux = function(self, ...)
    -- do things
end

Class methods, including the constructor, are defined as members of the class table.

The constructor is defined like this:

Class.new = function(...)
    local self = {
        member_a = default_value_member_a,
        -- member_b = nil,
    }
    return setmetatable(self, MT)
end

Example

-- Module for class 'Sum', if possible named 'sum.lua'.

--- Sum is a class to sum two values.
--- (Talk about over-engineering...)
-- @classmod sum

local M = {}
local MT = { __index = M }
local Sum = { methods = M }

--- Set the first value.
-- @tparam number value_a The first value.
M.set_a = function(self, value_a)
    self.value_a = value_a
end

--- Set the second value.
-- @tparam number value_b The second value.
M.set_b = function(self, value_b)
    self.value_b = value_b
end

--- Get sum of first and second value.
-- @treturn number The sum of the first and second values.
-- Can raise an error if the values cannot be summed.
M.get = function(self)
    return self.value_a + self.value_b
end

--- Example of a `__tostring` metamethod.
MT.__tostring = function(self)
    return string.format("sum: %s + %s", self.value_a, self.value_b)
end

--- Create a new sum.
Sum.new = function()
    return setmetatable({}, MT)
end

-- This module's only purpose is to define the class.
return Sum

If the module defined other classes as well, we would return them in a table like this:

return {
    Sum = Sum,
    ...
}

Usage of this module would be (with the first variant):

local Sum = require("sum")

s = Sum.new()

s:set_a(2)
s:set_b(3)
print(s:get())

Destructor

There are no destructors in Lua. You can rely on the gc metamethod which will be called when an object is being collected, but it MUST NOT be used as the primary way to release resources other than memory.

A common example is the case of a class File with a method close called when the object is destroyed. The developer still needs to call close() explicitly, since no RAII mechanism exists to call it silently.

Monkey-patching

Monkey-patching of Lima code is an anti-pattern and is forbidden.

Monkey-patching of non-Lima code should be as limited as possible (if you can write a wrapper instead, do it) and contained in a single module.

Overriding __tostring

In general it is not useful to override __tostring. If you do it, make sure the output is short and fits on a single line.

Inheritance

First, inheritance is a design-pattern which is powerful yet dangerous. Think twice if you need it and prefer to avoid it.

If necessary, inheritance should be explicitly declared. For instance, if B inherits from A, using:

setmetatable(B.methods, { __index = A.methods})

self:method() vs M.method(self)

self:method() gets the method method of the class of the instance self and calls it with self as the instance. M.method(self) gets the method method of class M and then calls it with self as instance.

The two statements do not perform the same if self's class is a child of M's. If this is the case, self:method() will call the method method of the child class and M.method(self) will call the method method of the parent class.

Example

-- A.lua

local M = {}
local MT = { __index = M }
local A = { methods = M }

M.foo = function(self)
    if SUPER then
        return M.foo(self)
    else
        return self:foo()
    end
end

M.bar = function(self)
    print("This is A!")
end

A.new = function()
    return setmetatable({}, MT)
end

return A
-- B.lua

local A = require "A"

local M = setmetatable({}, { __index = A.methods })
local MT = { __index = M }
local B = { methods = M }

M.bar = function(self)
    print("This is B!")
end

B.new = function()
    local self = A.new()
    return setmetatable(self, MT)
end

return B
-- example.lua

local A = require "A"
local B = require "B"

A.new():foo()
B.new():foo()

If SUPER is false, this will print:

This is A!
This is B!

If SUPER is true, this will print:

This is A!
This is A!
@winterwolf
Copy link

Two spaces and 80 chars max line length is a common lua standard. I totally disagree with the four spaces.

@catwell
Copy link
Author

catwell commented Feb 27, 2021

@winterwolf There is no common Lua standard. Actually a good chunk of the people who work on Lua and the core tools ident with three spaces. See the Kepler project and the LuaRocks code base for instance.

But this is a copy of a standard I wrote for internal code at a defunct company, so feel free to do whatever you want ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment