Classes
Do supports user-defined classes with fields, methods, getters and setters, multiple inheritance, and special methods to control behavior such as iteration and arithmetic operators.
Defining a Class
A class is defined with the class keyword, followed by a name and an
indented body containing field declarations and method definitions:
class Point
pub field x = 0
pub field y = 0
def (init) self x y
self.x = x
self.y = y
pub def distance self
(self.x * self.x + self.y * self.y)
Fields
Fields are declared with field inside the class body. A single declaration may
introduce one or more fields:
Defaults values are optional. A field declared without = ... is initialized
to nil.
Defaults are evaluated when an instance is created, once per field:
class Pair
pub field left right = []
let p = Pair()
p.left.push 1
assert_eq $p.left [1]
assert_eq $p.right []
Methods
Methods are defined with def inside the class body. The first parameter is
conventionally named self and receives the instance:
class Counter
field count = 0
pub def increment self
self.#count = (self.#count + 1)
pub def value self
self.#count
Computed Fields with getter and setter
Computed fields use decorators on methods:
class Config
field port = 8080
#[getter]
pub def port obj
obj.#port
#[setter]
pub def port obj value
obj.#port = value
Reads and writes still use ordinary field syntax:
See Decorators for decorator syntax and evaluation order, and
getter / setter for the
descriptor helpers.
Visibility
By default, all fields and methods of a class are private — they can only
be accessed from within the class's own methods. To make a field or method
accessible from outside the class, declare it with pub:
class Counter
field count = 0 # private field
def (init) self start
self.#count = start
pub def increment self # public method
self.#count = (self.#count + 1)
pub def value self # public method
self.#count
A class itself may also be declared pub to make it part of a module's public
API:
Private Fields
Fields declared without pub are private. Within the class, private fields are
accessed using the .#field syntax:
class BankAccount
field balance = 0
def (init) self initial
self.#balance = initial
pub def deposit self amount
self.#balance = (self.#balance + amount)
pub def balance self
self.#balance
The # explicitly signals a private access. Using .field (without #) on
self when the field is private produces a warning, and the compiler will
suggest using .#field instead.
Private Methods
Methods declared without pub are also private. Call them with .#method()
syntax from within the class:
class Adder
field base = 0
def (init) self base
self.#base = base
def double_base self # private helper
(self.#base * 2)
pub def add self x
(self.#double_base() + x)
let a = Adder 5
assert_eq $a.add(3) 13
Creating Instances
Call a class like a function to create an instance. Arguments are passed to
(init):
class Rectangle
pub field width = 0
pub field height = 0
def (init) self w h
self.width = w
self.height = h
pub def area self
(self.width * self.height)
let r = Rectangle 10 20
echo $r.area() # 200
echo $r.width # 10
Inheritance
A class can inherit from one or more parents by listing them after a colon. Let's start with a base class:
class Animal
pub field name = nil
pub field species = "unknown"
def (init) self name species
self.name = name
self.species = species
pub def describe self
"$(self.name) is a $(self.species)"
A child class inherits all fields and methods from its parent. Methods can be
overridden by redefining them. To call a parent method, use
Parent.method $self:
class Dog: Animal
pub field breed = nil
def (init) self name breed
Animal.(init) $self $name dog
self.breed = breed
pub def description self
"$(Animal.describe self) ($(self.breed))"
This results in the following behavior:
let rex = Dog "Rex" "German Shepherd"
echo $rex.describe() # Rex is a dog (German Shepherd)
# Call a parent method directly
echo $ Animal.describe $rex # Rex is a dog
Multiple Inheritance
List multiple parents after the colon, separated by spaces:
Superclass references may also be dotted names:
Member Resolution Order
- Earlier superclasses in the list win when the same member is defined multiple times
- A class's own members override inherited ones
- This rule is recursive: each parent brings along its already-merged inherited members.
For example:
Swapping the parents changes the result:
Calling Parent Constructor
Call the parent's (init) explicitly to initialize inherited fields:
class Cat: Animal
pub field indoor = false
def (init) self name indoor
Animal.(init) $self $name cat
self.indoor = indoor
Type Inspection
The type builtin works with classes:
let rex = Dog "Rex" "German Shepherd"
# Get the type of a value (returns the type object)
assert_eq (type rex) $Dog
# Test if a value is an instance of a class
assert (type rex Dog) # true: rex is a Dog
assert (type rex Animal) # true: Dog inherits from Animal
assert_not (type rex Cat) # false: Dog is not a Cat
See Basic Types for more on type.
Operator Overloading
Arithmetic, shift, bitwise, and comparison operators are dispatched to special methods. Define the method corresponding to the operator:
class Vec2
pub field x = 0
pub field y = 0
def (init) self x y
self.x = x
self.y = y
def (add) self other
Vec2 (self.x + other.x) (self.y + other.y)
def (sub) self other
Vec2 (self.x - other.x) (self.y - other.y)
def (mul) self scalar
Vec2 (self.x * scalar) (self.y * scalar)
def (shl) self count
Vec2 (self.x << count) (self.y << count)
def (shr) self count
Vec2 (self.x >> count) (self.y >> count)
def (neg) self
Vec2 (0 - self.x) (0 - self.y)
def (eq) self other
(self.x == other.x && self.y == other.y)
let a = Vec2 1 2
let b = Vec2 3 4
assert_eq (a + b) (Vec2 4 6)
assert_eq (b - a) (Vec2 2 2)
assert_eq (a * 3) (Vec2 3 6)
assert_eq (a << 1) (Vec2 2 4)
assert_eq (a >> 1) (Vec2 0 1)
assert_eq (-a) (Vec2 -1 -2)
assert (a == Vec2 1 2)
For binary operators, if the left operand does not define the method (because
it is a different type), the runtime tries the reverse variant on the right
operand. For example, 5 * myobj first tries int.(mul), and if that fails
for this operand type, falls back to myobj.(rmul):
| Forward | Reverse | Operator |
|---|---|---|
(sub) |
(rsub) |
- |
(div) |
(rdiv) |
/ |
(ediv) |
(rediv) |
// |
(mod) |
(rmod) |
% |
Shift operators do not have reverse variants. Use (shl) for << and (shr)
for >>.
Ordering: Defining (lt) and (eq) is sufficient for all four comparison
operators. <=, >, and >= are derived automatically:
class Num
pub field val = 0
def (init) self val
self.val = val
def (lt) self other
(self.val < other.val)
def (eq) self other
(self.val == other.val)
let n1 = Num 1
let n2 = Num 2
assert (n1 < n2)
assert (n1 <= n2)
assert (n2 > n1)
assert (n2 >= n1)
Special Method Reference
Special methods integrate class instances with language features. They are defined with the method name in parentheses.
(init): Constructor
Called when a new instance is created. Receives the new instance as the first argument:
(call): Function Call
Invoked when an instance is called like a function:
class Multiplier
field factor = 1
def (init) self factor
self.factor = factor
def (call) self x
(x * self.factor)
let double = Multiplier 2
echo (double 5) # 10
(unpack): Destructuring
Return a more primitive type (such as a dict) for the runtime to destructure
in lieu of self:
class Point
field x = 0
field y = 0
def (init) self x y
self.x = x
self.y = y
def (unpack) self
{x: self.x, y: self.y}
let p = Point 3 4
let :x :y = p
echo "$x, $y" # 3, 4
(iter): Obtain Iterator
Invoked implicitly by for loops, certain iterator combinators, etc. Should
return an object supporting the iteration protocol: either a built-in type, or
a class instance that implements (next):
class NumberRange
field start = 0
field stop = 0
def (init) self start stop
self.start = start
self.stop = stop
def (iter) self
(Range start: self.start end: self.stop).iter()
let r = NumberRange 0 5
assert_eq [...r] [0, 1, 2, 3, 4]
(next): Iterator Protocol
Invoked when getting the next item from an iterator. Returns the next value,
or throws IterStop when exhausted:
import std:
- IterStop
class Counter
field current = 0
field stop = 0
def (init) self start stop
self.current = start
self.stop = stop
def (iter) self
self
def (next) self
if (self.current >= self.stop)
throw IterStop()
let value = self.current
self.current = (self.current + 1)
value
An iterator should conventionally implement (iter) by returning self.
(sink): Obtain Sink
Invoked to obtain a sink object, such as by strand.put or
strand.redirect output: $instance
class ListCollector
field items = nil
def (init) self
self.items = []
def (sink) self
self.items.sink()
let collector = ListCollector()
redirect output: $collector do
put 1
put 2
put 3
assert_eq $collector.items [0, 1, 2]
(put): Sink Protocol
Invoked when an object is written to a sink.
A sink should conventionally implement (sink) by returning
self.
(bool): Boolean Conversion
Called when a value is used in a boolean context: if, while, !, &&,
||. Return a bool. If not defined, instances are always truthy:
class Vec2
pub field x = 0
pub field y = 0
def (init) self x y
self.x = x
self.y = y
def (bool) self
(self.x != 0 || self.y != 0)
let zero = Vec2 0 0
let nonzero = Vec2 1 0
assert_not (bool zero)
assert (bool nonzero)
(hash): Hash Code
Called by std.hash and when an instance is used as a dictionary key. Must
return an Int. If not defined, the hash is derived from the instance's
identity (memory address), consistent with the default identity-based equality.
std.hash accepts multiple values and hashes them all together in sequence,
which makes it easy to combine fields:
Important: if you define (eq), you should also define (hash) so that
equal objects produce the same hash:
import std:
- hash
class Point
pub field x = 0
pub field y = 0
def (init) self x y
self.x = x
self.y = y
def (eq) self other
(self.x == other.x && self.y == other.y)
def (hash) self
(self.x * 31 + self.y)
let p1 = Point 3 4
let p2 = Point 3 4
assert_eq (hash p1) (hash p2) # equal objects, equal hashes
# Can be used as dict keys
let d = {}
d[p1] = "hello"
assert_eq $d[p2] "hello"
(str): String Conversion
Called when an instance is converted to a string via str() or used in string
interpolation. Must return a Str. Falls back to (dbg) if not defined:
class Point
pub field x = 0
pub field y = 0
def (init) self x y
self.x = x
self.y = y
def (str) self
"($(self.x), $(self.y))"
let p = Point 3 4
echo "Point is $p" # Point is (3, 4)
(dbg): Debug String
Called for debug/inspect output and as a fallback when (str) is not defined.
Must return a Str. If neither (str) nor (dbg) is defined, the instance
displays as <object>:
(arg): External Command Argument
Called when an instance is interpolated into an external command as an argument
(e.g. echo $obj in a shell context). Must return a Str. Falls back to
(str) if not defined, which in turn falls back to (dbg):
class Path
pub field parts
def (init) self ...parts
self.parts = parts
def (arg) self
self.parts.join("/")
def (str) self
"Path($(self.parts.join("/")))"
(index) and (assign): Subscript Access
(index) is called for instance[key] reads; (assign) is called for
instance[key] = value writes:
class Table
pub field data = nil
def (init) self
self.data = {}
def (index) self key
self.data[key]
def (assign) self key value
self.data[key] = value
let t = Table()
t["x"] = 10
assert_eq $t["x"] 10
(get) and (set): Dynamic Field Fallback
Called when a field or method is accessed on an instance and no matching field,
method, or getter/setter exists. Receives self and the field name as a
symbol: