Decorated Methods
A method whose name begins with @ is a decorated method. The runtime
calls it for you when a piece of language syntax is applied to your
instance. That is how a class hooks into construction, arithmetic,
comparison and iteration without any special syntax of its own.
Decorated methods are ordinary methods. They can be inherited, overridden and called by name.
@new: Construction
Already covered. It runs when the class is called, receives the arguments,
and is the one place self.x = value may declare a new field.
Arithmetic
Define the operator you want and it works on your instances:
class Vector {
@new(x, y) {
self.x = x
self.y = y
}
@add(other) {
return Vector(self.x + other.x, self.y + other.y)
}
@sub(other) {
return Vector(self.x - other.x, self.y - other.y)
}
@mul(k) {
return Vector(self.x * k, self.y * k)
}
@neg() {
return Vector(-self.x, -self.y)
}
to_string() {
return '(${self.x}, ${self.y})'
}
}
var a = Vector(1, 2)
var b = Vector(3, 4)
echo (a + b).to_string()
echo (b - a).to_string()
echo (a * 3).to_string()
echo (-a).to_string()
(4, 6)
(2, 2)
(3, 6)
(-1, -2)
The full arithmetic set:
| Decorator | Operator |
|---|---|
@add | + |
@sub | - (binary) |
@mul | * |
@div | / |
@floordiv | // |
@mod | % |
@pow | ** |
@neg | - (unary) |
Bitwise and Logic
| Decorator | Operator |
|---|---|
@and | & |
@or | | |
@xor | ^ |
@lshift | << |
@rshift | >> |
@urshift | >>> |
@not | ~ |
class Flags {
@new(bits) {
self.bits = bits
}
@and(mask) {
return self.bits & mask
}
@not() {
return Flags(~self.bits)
}
}
echo Flags(5) & 4
echo (~Flags(5)).bits
4
-6
@not is bound to ~, the bitwise complement. ! is logical negation and
it is not overridable: an instance is always truthy, so !instance is
always false.
Comparison
| Decorator | Operator |
|---|---|
@eq | == and != |
@lt | < |
@lte | <= |
@gt | > |
@gte | >= |
class Version {
@new(major, minor) {
self.major = major
self.minor = minor
}
@lt(other) {
if self.major != other.major {
return self.major < other.major
}
return self.minor < other.minor
}
@gt(other) {
return other < self
}
}
echo Version(1, 2) < Version(1, 10)
echo Version(2, 0) > Version(1, 10)
true
true
Each Operator Is Separate
There is no derivation between them. Defining @lt does not give you
>, and defining @add does not give you += on the other side:
class Price {
@new(cents) {
self.cents = cents
}
@lt(other) {
return self.cents < other.cents
}
}
echo Price(250) < Price(500)
catch {
echo Price(500) > Price(250)
} as e {
echo e.message
}
true
operator '>' not defined for Price and Price
Define every operator you want to support. @gt is usually one line, as
the Version example above shows.
The Other Operand Is Not Checked
A decorated method is an ordinary method, and its parameter is an ordinary parameter. Nothing guarantees the other side is the same class:
class Amount {
@new(cents) {
self.cents = cents
}
@add(other) {
return Amount(self.cents + other.cents)
}
}
catch {
echo Amount(500) + 5
} as e {
echo '${e.type}: ${e.message}'
}
catch {
echo 5 + Amount(500)
} as e {
echo '${e.type}: ${e.message}'
}
TypeError: cannot read property 'cents' on a number
TypeError: operator '+' not defined for call signature (number, Amount)
Read those two together. With the instance on the left, @add ran and
failed inside your own method with a confusing message. With it on the
right, the operator was never dispatched to your class at all — a
decorated method only handles the case where its own instance is the left
operand.
Annotate the parameter to fix the first message, and accept the second as the rule:
class Sum {
@new(cents) {
self.cents = cents
}
@add(other: Sum) {
return Sum(self.cents + other.cents)
}
}
catch {
echo Sum(500) + 5
} as e {
echo e.message
}
@add() expects parameter 'other' (argument 1) to be a Sum, got number
Equality
@eq defines == for your class, and != is always its negation:
class Point {
@new(x, y) {
self.x = x
self.y = y
}
@eq(other) {
if !instance_of(other, Point) {
return false
}
return self.x == other.x and self.y == other.y
}
}
var p = Point(1, 2)
echo p == Point(1, 2)
echo p != Point(1, 2)
echo p == Point(2, 1)
echo p == nil
true
false
false
false
@eq runs only when the value on the right is an object too: a string, a
list, another instance and so on. p == nil, p == 5 and p == true
compare the ordinary way without calling it, so a nil check stays a nil
check whatever the class defines. It must return a bool; anything else
raises a TypeError. Without @eq, two instances are equal only when they
are the same object.
using matches through @eq as well, since it compares the way == does.
@eq decides == and nothing else. Lists and dictionaries compare the
instances inside them by identity, and so do contains(), index_of() and
dictionary keys:
class Point {
@new(x, y) {
self.x = x
self.y = y
}
@eq(other) {
if !instance_of(other, Point) {
return false
}
return self.x == other.x and self.y == other.y
}
}
var p = Point(1, 2)
echo [p] == [Point(1, 2)]
echo [p].contains(Point(1, 2))
echo [p].contains(p)
false
false
true
Iteration
@key and @value together make a class work with for ... in. They are
the largest of the decorated methods to get right, so they have a section
of their own: Making a Class Iterable.
| Decorator | Called by | Signature |
|---|---|---|
@key | for ... in | @key(previous) |
@value | for ... in | @value(key) |
@to_json
json.encode() calls @to_json() on an instance and encodes whatever it
returns:
import json
class User {
@new(name, password) {
self.name = name
self.password = password
}
@to_json() {
return { name: self.name }
}
}
echo json.encode(User('ada', 'hunter2'))
{"name":"ada"}
Without it, encoding an instance has nothing to work from. With it, you decide exactly what crosses the wire, which is the right place to leave a password behind.
@to_string
@to_string() decides what echo and print() show for an instance:
class Money {
@new(cents) {
self.cents = cents
}
@to_string() {
return '$' + (self.cents / 100)
}
}
var m = Money(500)
echo m
echo [m, Money(250)]
echo { total: m }
$5
[$5, $2.5]
{total: $5}
It applies wherever the instance sits in what is being shown, inside lists
and dictionaries, as a key or as a value. Without it, an instance shows as
<instance of Money>. It must return a string; anything else raises a
TypeError.
to_string()
to_string() has no @ because it is not a decorator; it is a real method
every value already has, and a class may override it to give its instances
a plain string form.
Nothing calls it for you. String interpolation and + render an instance
as <instance of Money>, so call it inside the interpolation. A class that
has both usually builds one from the other:
class Money {
@new(cents) {
self.cents = cents
}
to_string() {
return '$' + (self.cents / 100)
}
@to_string() {
return '<Money ${self.to_string()}>'
}
}
var m = Money(500)
echo m
echo 'cost: ${m.to_string()}'
<Money $5>
cost: $5
That is the split the standard library follows: to_string() is the value
as text, and @to_string() is how the instance looks when you print it.
Keep both cheap and free of side effects. Error messages, logging and debugging all reach for them.