Classes
Do supports user-defined classes with fields, methods, inheritance, and special methods to overload behavior such as iteration and calling.
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 let x = 0
pub let 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 let inside the class body. Each field has a default
value that is used when an instance is created:
!!! warning "Beware Mutable Default Values" Currently, all instances share the
same default field value. This is subject to change. Fields that are data
structures such as arrays or dictionaries should ideally be initialized to fresh
instances in the (init) method, described below.
Methods
Methods are defined with def inside the class body. The first parameter is
conventionally named self and receives the instance:
class Counter
let count = 0
pub def increment self
self.#count = (self.#count + 1)
pub def value self
self.count
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
let 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
let 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
let 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 let width = 0
pub let 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 extend another class by specifying a parent after a colon. Let's start with a base class:
class Animal
pub let name = nil
pub let 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 let 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
Calling Parent Constructor
Call the parent's (init) explicitly to initialize inherited fields:
class Cat: Animal
pub let indoor = false
def (init) self name indoor
Animal.(init) $self $name cat
self.indoor = indoor
Type Checking
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.
Special Methods
Special methods integrate class instances with language features. They are defined with the method name in parentheses.
Quick Reference
| Method | Trigger | Description |
|---|---|---|
(init) |
MyClass args... |
Constructor |
(call) |
instance args... |
Call instance as function |
(bool) |
if instance, !instance |
Boolean conversion |
(str) |
"$instance", str(instance) |
String conversion; fallback for (arg) |
(dbg) |
dbg function |
Debug string; fallback for (str) |
(arg) |
std.arg function, external program spawn |
Argument string |
(unpack) |
let :x :y = instance |
Destructuring |
(index) |
instance[key] |
Index |
(assign) |
instance[key] = val |
Index assign |
(get) |
instance.missing_field |
Dynamic field/method fallback |
(set) |
instance.missing_field = val |
Dynamic field assignment fallback |
(hash) |
std.hash(instance), dict key |
Hash code (must be consistent with (eq)) |
(iter) |
for x = instance, [...instance] |
Input iteration |
(next) |
iteration protocol | Advance input iterator |
(sink) |
redirect output: $instance |
Output iteration |
(put) |
output protocol | Advance output iterator |
Operators:
| Method | Operator | Notes |
|---|---|---|
(neg) |
-x |
Unary negation |
(bnot) |
~x |
Bitwise NOT |
(add) |
x + y |
|
(sub) |
x - y |
self is left operand |
(rsub) |
y - x |
self is right operand (left doesn't handle it) |
(mul) |
x * y |
|
(div) |
x / y |
self is left operand |
(rdiv) |
y / x |
self is right operand |
(ediv) |
x // y |
Euclidean division; self is left |
(rediv) |
y // x |
Euclidean division; self is right |
(mod) |
x % y |
self is left operand |
(rmod) |
y % x |
self is right operand |
(band) |
x & y |
Bitwise AND |
(bor) |
x \| y |
Bitwise OR |
(bxor) |
x ^ y |
Bitwise XOR |
(eq) |
x == y, x != y |
!= is the logical inverse |
(lt) |
x < y, x <= y, x > y, x >= y |
All four comparisons derived from (lt) and (eq) |
(hash) |
std.hash(x), dict key |
Must return an int; must be consistent with (eq) |
(init) --- Constructor
Called when a new instance is created. Receives the new instance as the first argument:
(call) --- Function Call
Makes an instance callable like a function:
class Multiplier
let 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 Do to destructure in lieu
of self:
class Point
let x = 0
let 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) --- Iteration
Makes an instance usable as an iterator source for for loops, spread syntax,
and so forth. Should return an object supporting the iteration protocol: either
a built-in type, or a class instance that implements (next):
class NumberRange
let start = 0
let 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
Defines a class as a stateful iterator. Return the next value, or throw
IterStop when exhausted:
import std:
- IterStop
class Counter
let current = 0
let 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) --- Sink Protocol
Makes an instance usable as a sink target with strand.put or
strand.redirect:
class ListCollector
let 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 Write Protocol
Receives values from put when the instance is used as 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 let x = 0
pub let 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 let x = 0
pub let 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 let x = 0
pub let 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 let 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 let 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
Operator Overloading
Arithmetic, bitwise, and comparison operators are dispatched to special methods. Define the method corresponding to the operator:
class Vec2
pub let x = 0
pub let 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 (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) (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) |
% |
Ordering: Defining (lt) and (eq) is sufficient for all four comparison
operators. <=, >, and >= are derived automatically:
class Num
pub let 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)
(get) and (set) --- Dynamic Field Fallback
Called when a field or method is accessed on an instance and no matching pub
field or method exists in the class prototype. Receives self and the field
name as a symbol:
class Dynamic
let data = {}
def (init) self
self.#data = {}
def (get) self key
self.#data[key]
def (set) self key value
self.data[key] = value
let d = Dynamic()
d.foo = 42
d.bar = "hello"
assert_eq $d.foo 42
assert_eq $d.bar "hello"
pub fields still take priority and are never routed through (get) or
(set):
class WithPub
pub let x = 0
def (init) self v
self.x = v
def (get) _self _key
"fallback"
let w = WithPub 10
# static pub field, not routed through (get)
assert_eq $w.x 10
assert_eq $w.missing "fallback"
(get) also serves as a fallback for method dispatch. When obj.method(args)
is called and method is not a statically defined method, the runtime calls
(get) to retrieve a callable and then invokes it directly with the provided
arguments. Any self-binding must be handled by (get) itself: