Quirrel4.41.0

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.

examples/pages/language/functions-minimal.nut
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))
Output:
addArmor(80, 40) = 120
totalArmor(80, 40) = 120

The 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.

examples/pages/language/functions-basics.nut
// 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")
Output:
computeDamage(100, 40) = 80
computeDamage(100, 40, 2.0) = 20
t34_tank carries 0 modules: 
t34_tank carries 2 modules: smoke_launcher, spare_tracks

Lambdas

@(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.

examples/pages/language/functions-lambdas.nut
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()))
Output:
ready squad names: alpha, bravo
table for alpha
type(nothing()) = table

this

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.

examples/pages/language/functions-this-basic.nut
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)
Output:
turret.fire() = 2
turret.ammo = 2

A method called through the instance that owns it gets that instance as this. A detached method gets the this that the caller supplies.

examples/pages/language/functions-this.nut
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())
Output:
aa_turret: 2 left
aa_turret: 1 left
aa_turret: 0 left

Attributes

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