Create: Computing AgesBrass Docs
The Brass language

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.

Brass
local function stacks(items)
  return items // 64
end
print(stacks(200))
print(stacks(1728))
Screen
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

Brass
local function name(parameter1, parameter2)
  -- the body: the code that runs at each call
  return parameter1 + parameter2
end

local 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:

writtenstored in
local function f() ... enda local variable f (the usual choice)
local f = function() ... endthe same, but the function cannot call itself by name
function f() ... enda global variable f, seen from every file until the computer reboots
function t.name() ... endthe field name of the table t
function t:name() ... endthe 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.

Brass
local function report(machine, rpm, unit)
  print(machine, rpm, unit)
end
report("press", 64)
report("saw", 128, "RPM", "extra")
Screen
press   64  nil
saw 128 RPM

That makes optional parameters easy: give them a default value at the start of the function with or.

Brass
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)
Screen
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:

Brass
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}))
Screen
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:

Brass
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")
Screen
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:

Brass
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({}))
Screen
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:

Brass
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)
end
Screen
drill   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.

Brass
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
Screen
> 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.

Brass
while true do
  write("> ")
  print(run(read()))
end

Methods: : 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.

Brass
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())
Screen
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:

Brass
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}))
Screen
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:

Brass
local function dig(depth)
  if depth == 0 then return "bedrock" end
  return dig(depth - 1)
end
print(dig(150))
print(dig(300))
Screen
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.

  1. Chunk-level locals, the local variables 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.
  2. The locals of a function, its parameters and the local variables declared in its body, are visible in that body only. Not in the functions written inside it.
  3. 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.
Brass
local function start_counting()
  local count = 0
  local function add_train()
    count = count + 1   -- error: count belongs to start_counting
  end
  add_train()
end

A nested function can still use its own parameters and locals, the chunk-level locals, and the globals. This is fine:

Brass
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")
Screen
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.sort that uses a local of the function around it;
  • two local function helpers 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:

Brass
local function show()
  print("speed: " .. tostring(speed))
end
local speed = 64   -- declared after show: show cannot see it
show()
Screen
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:

Brass
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()
Screen
[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.

Brass
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
end
Brass
local 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")
Screen
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:

Brass
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()
Screen
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:

Brass
-- the Lua way: does not compile in Brass
local function new_counter()
  local count = 0
  return function()
    count = count + 1
    return count
  end
end
Brass
local 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)
Screen
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:

Brass
-- 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")
Screen
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.