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

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:

DecoratorOperator
@add+
@sub- (binary)
@mul*
@div/
@floordiv//
@mod%
@pow**
@neg- (unary)

Bitwise and Logic

DecoratorOperator
@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

DecoratorOperator
@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.

DecoratorCalled bySignature
@keyfor ... in@key(previous)
@valuefor ... 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.