Functions
A function is a value. It can be stored in a table or an array, passed to another function, and returned. It captures the bindings that are visible where it is written.
Declaring one
function name(params) { ... } binds a name in the current scope. A parameter may have a default, and a trailing ... collects the rest of the arguments into vargv, an ordinary array. A default is evaluated once, when the closure is created. A table or array default is one object that every call without that argument shares, so do not mutate it.
Defaults may only come after the parameters without one, and a call must still pass every parameter that has no default.
A parameter and the return value may also carry a declared type - see Type annotations.
function addArmor(front, side) {
return front + side
}
// the same thing as a lambda: @(params) takes one expression and returns it
let totalArmor = @(front, side) front + side
println("addArmor(80, 40) =", addArmor(80, 40))
println("totalArmor(80, 40) =", totalArmor(80, 40))addArmor(80, 40) = 120
totalArmor(80, 40) = 120The same function written as a lambda takes the same arguments and returns the same value. @(params) expression is a shorter form for a function with one expression.
// damage falls off with distance; the falloff rate has a default
function computeDamage(baseDamage, distance, falloffPerMeter = 0.5) {
let reduced = baseDamage - distance * falloffPerMeter
return reduced > 0 ? reduced : 0
}
println("computeDamage(100, 40) =", computeDamage(100, 40))
println("computeDamage(100, 40, 2.0) =", computeDamage(100, 40, 2.0))
// a trailing ... collects the rest of the arguments into vargv
function describeLoadout(vehicleName, ...) {
println($"{vehicleName} carries {vargv.len()} modules: {", ".join(vargv)}")
}
describeLoadout("t34_tank")
describeLoadout("t34_tank", "smoke_launcher", "spare_tracks")computeDamage(100, 40) = 80
computeDamage(100, 40, 2.0) = 20
t34_tank carries 0 modules:
t34_tank carries 2 modules: smoke_launcher, spare_tracksLambdas
@(params) expression is a function with one expression. The value of the expression is the result. There is no return.
The body is an expression, so braces around it make a table literal, not a statement block. @() {} is a function that returns an empty table, not a function with an empty body. When the body needs statements, write function.
let squads = [
{ name = "alpha", strength = 4 },
{ name = "bravo", strength = 9 },
{ name = "charlie", strength = 2 },
]
// @(params) body takes one EXPRESSION, and its value is the result
let ready = squads.filter(@(squad) squad.strength >= 4)
println("ready squad names:", ", ".join(ready.map(@(squad) squad.name)))
// so a brace body is a table literal, not a statement block
let makeMarker = @(squad) { name = squad.name, kind = "squad" }
let marker = makeMarker(squads[0])
println($"{type(marker)} for {marker.name}")
// which means this returns an empty table, not null
let nothing = @() {}
println("type(nothing()) =", type(nothing()))ready squad names: alpha, bravo
table for alpha
type(nothing()) = tablethis
A function called through a table or an instance receives that container as this. A function taken out of its container and called on its own does not: this is then the value that the caller supplies. bindenv attaches a fixed this. call passes a this for one call.
class Turret {
ammo = 3
function fire() {
this.ammo -= 1 // this is the instance the method was called on
return this.ammo
}
}
let turret = Turret()
println("turret.fire() =", turret.fire())
println("turret.ammo =", turret.ammo)turret.fire() = 2
turret.ammo = 2A method called through the instance that owns it gets that instance as this. A detached method gets the this that the caller supplies.
let turret = {
name = "aa_turret"
ammo = 3
// a function stored in a table sees the table as this
function fire() {
if (this.ammo <= 0)
return $"{this.name}: empty"
this.ammo -= 1
return $"{this.name}: {this.ammo} left"
}
}
println(turret.fire())
println(turret.fire())
// pulled out of the table, the same closure loses its this
let detached = turret.fire
let rebound = detached.bindenv(turret)
println(rebound())aa_turret: 2 left
aa_turret: 1 left
aa_turret: 0 leftAttributes
A declaration can carry attributes in brackets. They tell the compiler what a call may assume: const function [pure] armorAt(angle) { ... }. See Function attributes.
Edge cases
- A parameter list is checked at the call. Too few or too many arguments throws
wrong number of parameters, and the message countsthis, so a two parameter function reports "3 required". - Only a function declared with
...gets its ownvargv. In a function without...,vargvis an ordinary captured name. It resolves to the nearest enclosing function with...(the top-level script always counts as one). It then holds the extra arguments of that outer function, not of the current call. - A named function expression,
let update = function updateSquad() {...}, names the closure for stack traces without bindingupdateSquadin the enclosing scope. cloneon a function returns the same function. Functions are immutable, so there is nothing to copy.