Functions
Name a piece of code and reuse it: parameters, return values, methods, recursion, and the scoping rule that sets Brass apart from Lua.
A function is a piece of code with a name, that you can run (call) as often as you like, with different values each time. You already call functions: print, sleep, rs.set. Writing your own lets you name a step of your program ("count the iron", "open the gate"), write it once, and use it everywhere.
local function stacks(items)
return items // 64
end
print(stacks(200))
print(stacks(1728))3 27
This page assumes you know values and variables and conditions and loops. If you come from Lua, read the scoping rule carefully: it is the one place where Brass behaves differently.
Defining and calling
local function name(parameter1, parameter2)
-- the body: the code that runs at each call
return parameter1 + parameter2
endlocal function creates the function and stores it in a local variable. Calling it is writing its name followed by the values in parentheses: name(3, 4). The parentheses are required even with no values: open_gate().
There are a few ways to write a function, all equivalent in what they make:
| written | stored in |
|---|---|
local function f() ... end | a local variable f (the usual choice) |
local f = function() ... end | the same, but the function cannot call itself by name |
function f() ... end | a global variable f, seen from every file until the computer reboots |
function t.name() ... end | the field name of the table t |
function t:name() ... end | the field name of t, as a method (see methods) |
Defining a function is an instruction, like an assignment: the function exists from the moment that line has run. Calling it on a line above its definition fails with attempt to call a nil value (global 'open_gate'). Put your functions at the top of the file and the main code at the bottom.
Parameters
The names in parentheses are the parameters: local variables of the function, filled with the values given at the call (the arguments), in order. A missing argument leaves its parameter at nil; extra arguments are ignored.
local function report(machine, rpm, unit)
print(machine, rpm, unit)
end
report("press", 64)
report("saw", 128, "RPM", "extra")press 64 nil saw 128 RPM
That makes optional parameters easy: give them a default value at the start of the function with or.
local function set_lamp(side, strength)
strength = strength or 15 -- full strength when not given
rs.set(side, strength)
print(side .. " lamp at " .. strength)
end
set_lamp("top")
set_lamp("left", 7)top lamp at 15 left lamp at 7
The or trick replaces false as well as nil. For a parameter where false is a real choice, write if enabled == nil then enabled = true end instead.
Brass functions take a fixed list of parameters: ... (any number of arguments) is a compile error, variable arguments ('...') are not supported. To pass "any number of things", pass one table:
local function total(counts)
local sum = 0
for _, n in ipairs(counts) do
sum = sum + n
end
return sum
end
print(total({64, 64, 12}))140
Returning a value
return value ends the function and hands the value back to the caller, where the call stands for it. A function that ends without return, or with return alone, gives nil.
A Brass function returns a single value. return a, b does not compile: a function returns a single value (return a table instead). When you have several things to give back, put them in a table and read its fields:
local function split_stacks(items)
return {stacks = items // 64, rest = items % 64}
end
local result = split_stacks(200)
print(result.stacks .. " stacks and " .. result.rest .. " items")3 stacks and 8 items
return must be the last statement of its block. To leave a function early, put it inside an if; this "guard" style keeps the normal case unindented:
local function average(readings)
if #readings == 0 then
return nil -- nothing to average
end
local sum = 0
for _, r in ipairs(readings) do
sum = sum + r
end
return sum / #readings
end
print(average({60, 64, 68}), average({}))64 nil
Functions are values
A function is a value like a number or a string. You can store it in a variable or a table, and pass it to another function. A function written without a name, function(a, b) ... end, is an anonymous function: handy when it is only needed in one place.
The best-known case is table.sort, which takes a function that says whether a must come before b:
local machines = {
{name = "press", stress = 512},
{name = "fan", stress = 128},
{name = "drill", stress = 1024},
}
table.sort(machines, function(a, b) return a.stress > b.stress end)
for _, m in ipairs(machines) do
print(m.name, m.stress)
enddrill 1024 press 512 fan 128
A command dispatcher
Functions stored in a table, under the name of a command, make a dispatch table: the program looks the command up and calls what it finds, instead of a long chain of if ... elseif. Adding a command is adding a function.
local gate_open = false
local commands = {}
function commands.open()
gate_open = true
rs.set("back", true)
return "gate opened"
end
function commands.close()
gate_open = false
rs.set("back", false)
return "gate closed"
end
function commands.status()
if gate_open then return "the gate is open" end
return "the gate is closed"
end
function commands.help()
local names = table.keys(commands)
table.sort(names)
return "commands: " .. table.concat(names, ", ")
end
local function run(line)
local action = commands[line]
if action == nil then
return "unknown command '" .. line .. "', try help"
end
return action()
end
for _, typed in ipairs({"help", "open", "status", "explode"}) do
print("> " .. typed)
print(run(typed))
end> help commands: close, help, open, status > open gate opened > status the gate is open > explode unknown command 'explode', try help
In the real program, the player types the commands: replace the last loop with a main loop that reads them.
while true do
write("> ")
print(run(read()))
endMethods: : and self
A table can hold both data and the functions that work on it. Such a function is a method, and Brass has a shortcut for it: function door:toggle() is the same as function door.toggle(self), and the call door:toggle() is the same as door.toggle(door). The colon passes the table itself as a hidden first parameter, named self.
local door = {side = "back", open = false}
function door:toggle()
self.open = not self.open
rs.set(self.side, self.open)
if self.open then return "door opened" end
return "door closed"
end
print(door:toggle())
print(door:toggle())door opened door closed
Calling a method with a dot, door.toggle(), passes nothing as self and fails inside the method with attempt to index a nil value (local 'self'). When you see that message, look for a . that should be a :.
Methods make objects: tables that carry their own state. Tables shows how to make many objects of the same kind.
Recursion
A function can call itself: that is recursion. It fits things that contain smaller things of the same kind, like crates inside crates:
local function count_items(crate)
local total = 0
for _, thing in ipairs(crate) do
if type(thing) == "table" then
total = total + count_items(thing) -- a crate inside the crate
else
total = total + thing
end
end
return total
end
print(count_items({64, 32, {16, 16, {8}}, 4}))140
A local function can call itself by name; local f = function() ... end cannot (inside, f is not declared yet and means a global f, which is nil).
The call depth limit
Each call that has not returned yet takes a place on the call stack, and the stack holds 200 calls (the main program counts as one). Going deeper stops the program with stack overflow:
local function dig(depth)
if depth == 0 then return "bedrock" end
return dig(depth - 1)
end
print(dig(150))
print(dig(300))bedrock snippet:3: stack overflow
Brass has no tail calls: return dig(depth - 1) still uses a place, unlike in Lua. Each waiting call also uses 8 cells of memory. For work that can go deep (filling an area block by block, walking a long chain), use a loop and a list of things still to do instead of recursion.
The scoping rule
This is the rule that sets Brass apart from Lua, so it is worth reading slowly.
- Chunk-level locals, the
localvariables written at the top level of a file (outside any function), are shared by all the functions of that file: every function can read and change them. - The locals of a function, its parameters and the
localvariables declared in its body, are visible in that body only. Not in the functions written inside it. - A function written inside another function that uses one of the outer function's locals does not compile:
cannot capture local 'count' of an enclosing function, with the file and the line in front.
local function start_counting()
local count = 0
local function add_train()
count = count + 1 -- error: count belongs to start_counting
end
add_train()
endA nested function can still use its own parameters and locals, the chunk-level locals, and the globals. This is fine:
local unit = "RPM" -- chunk-level: every function sees it
local function report(machines)
local function line(m) -- a nested function using its parameter and a chunk-level local
return m.name .. ": " .. m.rpm .. " " .. unit
end
for _, m in ipairs(machines) do
print(line(m))
end
return #machines
end
print(report({{name = "press", rpm = 64}, {name = "fan", rpm = 128}}) .. " machines")press: 64 RPM fan: 128 RPM 2 machines
Why this rule? A function's locals live in that call's own space, which is freed the moment the function returns. Without closures, nothing can keep that space alive behind your back: memory stays easy to predict on machines that count every cell.
Where it bites, in practice:
- a helper function inside another function that uses its parameters;
- an anonymous comparator for
table.sortthat uses a local of the function around it; - two
local functionhelpers inside a function that call each other (the name of the first one is a local too); - a function created in a loop, inside a function, that uses the loop variable.
Order matters at chunk level
A function sees the chunk-level locals declared above it in the file. A name declared further down is not known yet when the function is written, so it means a global instead:
local function show()
print("speed: " .. tostring(speed))
end
local speed = 64 -- declared after show: show cannot see it
show()speed: nil
Declare the state of your program at the top of the file. When two functions need each other, or you like to keep small helpers at the bottom, declare the name first and fill it later:
local log -- declared here, defined below
local function open_gate()
rs.set("back", true)
log("gate opened")
end
function log(text) -- fills the local declared above
print("[gate] " .. text)
end
open_gate()[gate] gate opened
Code that would need a closure
In Lua, a function often keeps state in the locals of the function that created it (a closure). In Brass, put that state somewhere a function is allowed to reach. Three ways, from the simplest.
1. Pass the value as a parameter. Move the helper out to the file level and give it what it needs.
local function show_all(machines, unit)
local function line(m)
return m.name .. ": " .. m.rpm .. " " .. unit -- error: unit belongs to show_all
end
for _, m in ipairs(machines) do print(line(m)) end
endlocal function line(m, unit)
return m.name .. ": " .. m.rpm .. " " .. unit
end
local function show_all(machines, unit)
for _, m in ipairs(machines) do
print(line(m, unit))
end
end
show_all({{name = "press", rpm = 64}, {name = "fan", rpm = 128}}, "RPM")press: 64 RPM fan: 128 RPM
2. Keep the state at chunk level. A comparator for table.sort cannot take extra parameters, so the key to sort by goes into a chunk-level local that both functions see:
local sort_key = "name"
local function by_key(a, b)
return a[sort_key] < b[sort_key]
end
local function sort_by(list, key)
sort_key = key
table.sort(list, by_key)
end
local machines = {
{name = "press", stress = 512},
{name = "fan", stress = 128},
{name = "drill", stress = 1024},
}
sort_by(machines, "stress")
for m in machines do write(m.name, " ") end
print()
sort_by(machines, "name")
for m in machines do write(m.name, " ") end
print()fan press drill drill fan press
3. Keep the state in a table. When you need several independent copies of the state (one counter per gate, one record per machine), each copy is a table, and the functions receive it. With methods, the table arrives as self:
-- the Lua way: does not compile in Brass
local function new_counter()
local count = 0
return function()
count = count + 1
return count
end
endlocal function new_counter(name)
local counter = {name = name, count = 0}
function counter:add()
self.count = self.count + 1 -- self, not counter: self is add's own parameter
end
return counter
end
local north = new_counter("north gate")
local south = new_counter("south gate")
north:add()
north:add()
south:add()
print(north.name .. ": " .. north.count)
print(south.name .. ": " .. south.count)north gate: 2 south gate: 1
Writing counter.count inside add would be the capture error again: counter is a local of new_counter. Through self, the method only uses its own parameter.
A state machine for a door
Many automations are state machines: the device is in one state at a time, and each event moves it to another state, or not. A big door with pistons is closed, opening, open or closing. Writing the transitions as a table keeps them in one place, and a small function applies them:
-- for each state, the event that leads to the next state
local transitions = {
closed = {button = "opening"},
opening = {done = "open"},
open = {button = "closing", timeout = "closing"},
closing = {done = "closed", blocked = "opening"},
}
local state = "closed"
local function handle(event)
local next_state = transitions[state][event]
if next_state == nil then
print(state .. ": ignores " .. event)
return
end
print(state .. " -> " .. next_state)
state = next_state
end
handle("button")
handle("button") -- already opening: nothing to do
handle("done")
handle("timeout")
handle("blocked") -- a player in the way: open again
handle("done")closed -> opening opening: ignores button opening -> open open -> closing closing -> opening opening -> open
state is a chunk-level local, so handle can change it. In the world, the events come from os.pull_event: a redstone event from the button, a timer event for the timeout (see Events), and the door sets its redstone outputs when it enters a new state.
Functions in other files
A program can be split into several files: import runs another file once and gives back what it returns, usually a table of functions. Each file keeps its own chunk-level locals; globals are shared. See Modules.