Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Defining a Class

class Account {
  var balance = 0
  var owner

  @new(owner, balance) {
    self.owner = owner
    self.balance = balance or 0
  }

  deposit(amount) {
    self.balance += amount
    return self
  }
}
  • class Name { ... } declares it. PascalCase is the convention.
  • var inside the body declares a field, with an optional default. A field with no default starts as nil.
  • A bare name(params) { ... } is a method. There is no def keyword on methods.
  • @new is the constructor.
  • self is the current instance.

Creating an Instance

Call the class. There is no new keyword:

var a = Account('Ada', 50)

echo a.owner
echo a.balance
Ada
50

If the class has no @new, calling it with no arguments gives you an instance with every field at its default.

The Constructor

@new is the constructor. It runs once, when the class is called, and its job is to put the instance into a usable state — which is what Account’s did above.

It Is Optional

A class with no @new is constructed with no arguments, and every field takes its declared default:

class Settings {
  var theme = 'dark'
  var retries
}

var s = Settings()

echo s.theme
echo s.retries
dark
nil

Extra arguments to a class with no @new are ignored rather than rejected, exactly as they are for a function.

It Declares Fields

self.x = value inside @new declares a field. This is the one place in the language where assignment creates a member, and it exists so a constructor does not have to repeat every field as a var line above it:

class Point {

  @new(x, y) {
    self.x = x
    self.y = y
    self.distance = (x * x + y * y).sqrt()
  }
}

var p = Point(3, 4)

echo p.distance
5

distance was never declared with var, and it is a real field.

That power belongs to @new’s own body and nowhere else. A constructor that delegates its setup to a helper must declare those fields:

class Delayed {
  var ready          # required: setup() cannot declare it

  @new() {
    self.setup()
  }

  setup() {
    self.ready = true
  }
}

echo Delayed().ready
true

Remove the var ready line and setup() raises undefined field 'ready'.

Arguments Are Not Checked Unless You Ask

@new is a function, so the usual rules apply: missing arguments arrive as nil, extra ones are dropped. A constructor that assumes it got something fails later, in a confusing place:

class Ctor {

  @new(x) {
    self.derived = x * 2
  }
}

Ctor()
Unhandled TypeError: operator '*' not defined for call signature (nil, float)

Annotate the parameter and the failure moves to the call, where it names the problem:

class Ctor {

  @new(x: number) {
    self.derived = x * 2
  }
}

echo Ctor(5).derived

catch {
  Ctor('five')
} as e {
  echo e.message
}
10
@new() expects parameter 'x' (argument 1) to be a number, got string

Constructors are the highest-value place in a program to annotate, because a badly built object goes wrong somewhere else entirely.

Validating in the Constructor

A constructor may raise. Nothing is returned to the caller, so an object that cannot be valid never exists:

class Port {

  @new(number: number) {
    if number < 1 or number > 65535 {
      raise ValueError('port out of range: ${number}')
    }

    self.number = number
  }
}

echo Port(8080).number

catch {
  Port(99999)
} as e {
  echo '${e.type}: ${e.message}'
}
8080
ValueError: port out of range: 99999

This is worth doing whenever a class has an invariant. Every other method can then assume it holds, instead of re-checking.

@new Cannot Return a Value

Calling a class always produces an instance of that class. A return in @new ends the constructor early; it does not change what the caller gets:

class Returns {

  @new() {
    self.v = 1

    return 'this is discarded'
  }
}

echo typeof(Returns())
Returns

If you want a call that may hand back something else — a cached instance, a subclass, nil on bad input — write a static factory method and call that instead.

Methods

A method is a name(params) { ... } declaration inside the class body. There is no def keyword on it:

class Account {

  @new(owner, balance) {
    self.owner = owner
    self.balance = balance
  }

  deposit(amount) {
    self.balance += amount
    return self
  }
}

var a = Account('Ada', 50)

a.deposit(25).deposit(25)

echo a.balance
100

deposit returns self, which is what makes the chain work. Returning self from a mutator is a common Zuri idiom.

Inside a method, self is required to reach a field or another method. There is no implicit receiver:

class Greeter {
  var name = 'world'

  greet() {
    return 'hello ' + self.name
  }
}

Writing name there would look for a local or a global, not a field.

Fields Are Declared, Not Discovered

The set of fields is fixed when the class is declared. Two things follow from that.

First, assigning to a field that was never declared is an error:

class Account {
  var balance = 0
}

var a = Account()

catch {
  a.nickname = 'rainy day'
} as e {
  echo e.message
}
undefined field 'nickname' on instance of 'Account'

Second, self.x = value inside @new does declare a field, as a convenience so that constructors do not have to repeat themselves:

class Point {
  @new(x, y) {
    self.x = x
    self.y = y
  }
}

echo Point(3, 4).x
3

That convenience applies to @new’s own body and nowhere else. A constructor that calls a helper to do its initialisation must declare those fields with var:

class A {
  var from_helper          # required

  @new() {
    self.setup()
  }

  setup() {
    self.from_helper = 2
  }
}

Without the var line, the assignment inside setup() raises undefined field 'from_helper'.

Static Members

static puts a field or method on the class rather than on each instance:

class Account {
  static var count = 0

  @new(owner) {
    self.owner = owner
    Account.count++
  }

  static open(owner) {
    return Account(owner)
  }
}

Account('Ada')
Account('Bob')

echo Account.count
echo Account.open('Carol').owner
2
Carol

A static method has no self:

SyntaxError: 'self' used outside of a method

Reach the class by name instead.

Static fields are the one mutable part of a class. The set of members is sealed; the values of static fields are not.

Duplicate Members Are Errors

Declaring the same method twice in one class does not silently keep the last one:

class B {
  m() {}
  m() {}
}
SyntaxError: multiple declaration for method 'm' found in class 'B'

The same applies to declaring the same class name twice in one module.

Instances and Dictionaries

dictclass
keysany, added at any timefixed at declaration
accessd.key or d['key']obj.field only
missing memberget() returns a fallbackerror
behaviournonemethods
cost of a readhash lookuparray index

Use a dictionary for data whose shape you learn at runtime: parsed JSON, HTTP headers, a config file. Use a class when the shape is known and there is behaviour to attach.