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

Types and methods

type defines a custom type with fields and methods. The parentheses list fields supplied at construction; a field may have a default value. Every method must declare self as its first parameter. Type definitions belong at module scope, not inside a function or a control-flow block.

type Point(x, y=0) {
    func move(self, dx, dy) {
        self.x = self.x + dx
        self.y = self.y + dy
    }

    func text(self) {
        return "(" + Str(self.x) + ", " + Str(self.y) + ")"
    }
}

var p = Point(1)
p.move(2, 3)
print(p.text())

Instance fields can be read and updated directly. Construction accepts both positional and named arguments:

var origin = Point(x=0, y=0)
origin.x = 10

Required fields must come before fields with defaults. Calling a type requires all required fields exactly once; named arguments make construction clearer when a type has several fields.

Methods and state

Methods are ordinary functions attached to a type. They can read or replace fields through self, and methods may call other methods on the same instance.

type Counter(value=0) {
    func increment(self) {
        self.value = self.value + 1
        return self.value
    }
}

var counter = Counter()
print(counter.increment()) # 1
print(counter.increment()) # 2

Protocol methods

Goblin lets custom types participate in operations and protocols through conventionally named methods such as __add, __cmp, __bool, __str, __iter, and __getitem. These names have leading double underscores only; there are no trailing underscores. Most programs can start with ordinary fields and methods.

The most useful protocol methods are shown below. Their parameter shapes are fixed: use self alone for conversion and iteration methods, self, other for binary operators and comparison, and self, index, value for __setitem.

MethodEnables
__add, __sub, __mul, __div, __modArithmetic operators
__radd, __rsub, __rmul, __rdiv, __rmodThe same operators with the instance on the right
__notLogical ! operator; without it ! negates truthiness
__cmp==, !=, <, <=, >, >=; return -1, 0, or 1. Consulted from either side of a comparison
__strPrinting and Str(value)
__boolConditions, Bool(value), and the truthiness test in && / `
__iterfor value in instance
__getitem, __setiteminstance[index] read and assignment

If a protocol method is absent, the corresponding operation raises TypeError. Equality is the exception: without __cmp, == and != fall back to identity, so an instance is equal only to itself and never raises. Ordering comparisons (<, <=, >, >=) still require __cmp.

With __cmp, a type mismatch inside it still reads as "unequal" — money == nil stays false for a __cmp written only for numbers — but any other failure is reported rather than swallowed, so == raises whatever the method raised.

A comparison consults both operands, so the instance does not have to be on the left. When the left operand has no ordering for the right one — as a built-in number never has for a custom type — the right operand is asked to compare itself against the left and the result is flipped:

type Money(amount) {
    func __cmp(self, other) {
        return self.amount - other
    }
}

var m = Money(5)
print(m < 10) # true
print(10 > m) # true, answered by Money.__cmp

Arithmetic cannot flip its operands the same way — a - b is not b - a — so it uses a second set of methods instead. When the left operand does not know the right one, the right operand's __radd, __rsub, __rmul, __rdiv or __rmod is called with the left operand as its argument:

type Scaled(factor) {
    func __mul(self, other) {
        return Scaled(self.factor * other)
    }
    func __rmul(self, other) {
        return Scaled(other * self.factor)
    }
}

var s = Scaled(3)
print(s * 2) # Scaled(6)
print(2 * s) # Scaled(6), answered by __rmul

A reflected method is only reached after the left operand reports that the types do not fit. Anything else it raises — a division by zero, a failure inside its own body — propagates instead, and an operator whose reflected method is not defined keeps reporting the error of its left operand.

The built-in sequences follow the same rule, so repetition reads in either order: 3 * "ab" and 2 * [1, 2] work like "ab" * 3 and [1, 2] * 2.

For example, this type supports + and printing:

type Vector(x, y) {
    func __add(self, other) {
        return Vector(self.x + other.x, self.y + other.y)
    }

    func __str(self) {
        return "Vector(" + Str(self.x) + ", " + Str(self.y) + ")"
    }
}

print(Vector(1, 2) + Vector(3, 4))