Quirrel4.41.0

Classes and instances

A class is a value like any other. It can be stored in a variable, passed to a function, and kept in a table or array. It describes the fields and methods of every instance made from it.

Declaring a class

class Name { ... } declares fields with a default value and methods, either as function name() { ... } or as a field assigned a function or a lambda. constructor is a reserved method name, called automatically for every new instance. Inside a method, a field is always reached through this.. Unlike some other object-oriented languages, a bare name does not mean this.name.

examples/pages/language/classes-basics.nut
class Weapon {
  weaponName = "unknown"
  ammoBelt = 0
  constructor(weaponName, ammoBelt) {
    this.weaponName = weaponName
    this.ammoBelt = ammoBelt
  }
  function fire() {
    if (this.ammoBelt <= 0)
      return $"{this.weaponName}: empty"
    this.ammoBelt -= 1
    return $"{this.weaponName}: {this.ammoBelt} left"
  }
}

let rifle = Weapon("ak74", 2)
println(rifle.fire())
println(rifle.fire())
println("rifle instanceof Weapon =", rifle instanceof Weapon)
Output:
ak74: 1 left
ak74: 0 left
rifle instanceof Weapon = true

Inheritance and base

A derived class copies every field, static and method from its base first, then applies its own body over that: class Tank(Vehicle) { ... }. base reaches the shadowed implementation from inside an override, most often base.constructor(...) or base.someMethod(...). There is no super keyword. super is an unknown variable and fails to compile.

A derived class that declares no constructor of its own inherits the base's, called with the arguments the instantiation call passes. getbase() returns the class given in the parentheses, or null for a class with none.

examples/pages/language/classes-inheritance.nut
class Vehicle {
  hitPoints = 100
  function describe() {
    return $"{this.hitPoints} hp"
  }
}

// (Vehicle) copies Vehicle's members first, then applies the rest of the body
class Tank(Vehicle) {
  turretAngle = 0
  constructor() {
    this.hitPoints = 250
  }
  // base reaches the overridden implementation; there is no super keyword
  function describe() {
    return base.describe() + $", turret {this.turretAngle}"
  }
}

let abrams = Tank()
println(abrams.describe())
println("abrams instanceof Vehicle =", abrams instanceof Vehicle)
Output:
250 hp, turret 0
abrams instanceof Vehicle = true

The keyword extends appears in the grammar (class Tank extends Vehicle) but the lexer never produces it. It is dead syntax and fails to parse with expected '{'. Use the parenthesized form above.

Static members

static name = value, written inside the class body, is one value shared by every instance, not a per-instance default. A static is read-only after declaration. ClassName.name = value throws trying to set 'class'. this.name = value from a method throws the index 'name' (type='string') does not exist, because a static is not stored per instance. The only way to add or replace a static after the class exists is newmember with its third argument set to true.

Instantiation

ClassName(args) creates an instance, copies the class's field defaults into it, and runs constructor if one is declared. The defaults are copied as they are, not cloned. If a default is an array or a table and no constructor replaces it, every instance that keeps that default shares the same array. Give a mutable default its own value inside constructor when each instance needs an independent one. instance() makes an instance the same way, without running constructor.

Every instance remembers the class it was made from. getclass() returns that class, never a base class, even for a class made with class Tank(Vehicle) { ... }.

instanceof

x instanceof ClassName walks x's class and its bases. It does not throw when x is not an instance: 5 instanceof Squad is a legal false. It does throw when the right-hand side is not a class value: squad instanceof 5 throws cannot apply instanceof between a integer and a instance (the message names the right operand's type first, then the left's). An instance's instanceof against the built-in placeholder Instance class from the types module is always false, regardless of the class that made it. See Values and types for the reason, and for the type(x) == "instance" check that tests whether a value is any instance.

Locking

A class locks permanently the first time it gains an instance, becomes the base of another class, or has lock() called on it directly, whichever happens first. After the lock, a new plain field cannot be added: ClassName.newField <- value throws trying to modify a class that has already been instantiated, inherited or is locked manually. The lock does not stop a method from being added or replaced the same way, since <- with a function value bypasses the lock check. It also does not stop newmember from adding a new static, when its third argument asks for one.

Metamethods

A class can define any of the seventeen metamethods the VM recognizes (_add _sub _mul _div _unm _modulo, _set _get _newslot _delslot, _typeof _cmp _nexti _call _cloned _tostring, and _lock) as an ordinary method with that name. getmetamethod looks one up by name without calling it. All of them run with an instance as this except _lock. _lock runs once, at the moment the class itself locks, with the class object as this. It is the only metamethod that never fires on an instance.

Metamethods gives the contract of each one: when the VM calls it, what it receives, and what it must return or throw.

examples/pages/language/classes-metamethods.nut
class Squad {
  squadName = "unknown"
  strength = 0
  constructor(squadName, strength) {
    this.squadName = squadName
    this.strength = strength
  }
  // array.sort() with no comparator falls back to this
  function _cmp(other) {
    return this.strength - other.strength
  }
  // println and string concatenation both call this instead of printing an address
  function _tostring() {
    return $"{this.squadName}({this.strength})"
  }
}

let squads = [Squad("alpha", 9), Squad("bravo", 4), Squad("charlie", 6)]
squads.sort()
foreach (squad in squads)
  println(squad)
Output:
bravo(4)
charlie(6)
alpha(9)

Edge cases