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

Inheritance

A class extends another with <:

class Shape {
  @new(name) {
    self.name = name
  }

  area() {
    raise NotImplementedError('${self.name} must define area()')
  }

  describe() {
    return '${self.name} with area ${self.area()}'
  }
}

class Circle < Shape {
  @new(radius) {
    parent('circle')
    self.radius = radius
  }

  area() {
    return 3.141592653589793 * self.radius ** 2
  }
}

echo Circle(1).describe()
circle with area 3.141592653589793

A subclass inherits every field and method. Single inheritance only; a class has at most one parent.

Note the direction of the arrow. class Circle < Shape creates a new class that borrows from Shape. Turning it around — class Anything > Shape — is a different declaration entirely: it adds methods to Shape itself and creates no class at all. See Class Extensions.

Naming the Parent

A class is a value like any other, so the parent does not have to be a bare name in the current file. Anything an expression starting with a name can reach works, which is what lets you build on a class another module owns:

import log

class MemoryTransport < log.Transport {
  @new() {
    self.lines = []
  }

  handle(record) {
    self.lines.append(record)
  }
}

echo instance_of(MemoryTransport(), log.Transport)
true

Property access, indexing and calls all work the same way, so a parent picked out of a registry (class Store < backends['redis']) or handed back by a function (class Store < chosen_backend()) is as valid as a name. The expression is evaluated once, where the class is declared.

The one rule is that it has to begin with a name. That is what keeps the { opening the body from ever being read as the start of a dictionary.

parent

parent means two related things.

parent(args) calls the parent’s constructor. That is the parent('circle') line in Circle above, and it comes first in @new, before the subclass sets up anything of its own. Calling it late means the parent’s constructor overwrites what you just assigned; not calling it at all means the parent’s fields are never initialised.

parent.method(args) calls the parent’s version of a method that this class has overridden:

class Square < Shape {
  @new(side) {
    parent('square')
    self.side = side
  }

  area() {
    return self.side ** 2
  }

  describe() {
    return 'a ' + parent.describe()
  }
}

echo Square(2).describe()
a square with area 4

Note what happened there. parent.describe() ran Shape’s describe, which calls self.area(), which dispatched back to Square’s area. A method always dispatches on the actual object, not on the class the code was written in.

Overriding

Redeclaring a method in a subclass replaces it. There is no keyword for it and no way to forbid it.

describe() above is the useful shape of this: a parent method written in terms of a method the child supplies. Shape.area() raises NotImplementedError, so a subclass that forgets to override it says so clearly:

catch {
  Shape('blob').area()
} as e {
  echo e.message
}
blob must define area()

That is Zuri’s abstract method. There is no abstract keyword because there does not need to be one.

A Subclass Without a Constructor

If a subclass declares no @new, it uses its parent’s:

class NoCtor < Shape {}

echo NoCtor('plain').name
plain

Testing Ancestry

instance_of() walks the whole chain:

echo instance_of(Circle(1), Shape)
echo instance_of(Circle(1), Square)
true
false

typeof() gives the most specific class name, as a string:

echo typeof(Circle(1))
Circle

The Depth Is Free

Inheritance chains cost nothing to walk at runtime. A method lookup on a class four levels deep is the same operation as one on a class with no parent, because every class’s method table is complete at declaration time. Write the hierarchy the design wants.

What You Write Instead of an Interface

Zuri has single inheritance and no interfaces, so the two patterns below do the jobs an interface would do elsewhere.

A base class that raises. When a base class needs every subclass to supply a method, declare it and raise:

class Shape {

  @new(name: string) {
    self.name = name
  }

  area() {
    raise NotImplementedError('${self.name} must define area()')
  }

  describe() {
    return '${self.name} has area ${self.area()}'
  }
}

class Circle < Shape {

  @new(radius: number) {
    parent('circle')
    self.radius = radius
  }

  area() {
    return 3.141592653589793 * self.radius ** 2
  }
}

class Blob < Shape {

  @new() {
    parent('blob')
  }
}

echo Circle(2).describe()

catch {
  echo Blob().describe()
} as e {
  echo '${e.type}: ${e.message}'
}
circle has area 12.566370614359172
NotImplementedError: blob must define area()

describe() calls self.area(), and self is the actual instance, so the subclass’s version runs. That is the whole of dynamic dispatch in Zuri: there is nothing to declare and nothing to mark virtual.

A parameter typed by the base class. An annotation naming a class accepts any subclass of it, which is how you say “anything that is a Shape”:

def total_area(shapes: list) {
  return shapes.reduce(@(sum, shape: Shape) => sum + shape.area(), 0)
}

Checking What Something Is

Two built-ins answer questions about an instance’s type, and they answer different questions.

instance_of(value, Class)

instance_of() asks “does this behave like a Class?”, and it walks the whole inheritance chain to answer:

class Shape {}
class Circle < Shape {}
class Square < Shape {}
class Ellipse < Circle {}

var c = Circle()

echo instance_of(c, Circle)
echo instance_of(c, Shape)
echo instance_of(c, Square)
echo instance_of(Ellipse(), Shape)
true
true
false
true

Ellipse is two levels below Shape and still answers true. There is no depth limit: the check follows parents until it finds a match or runs out. A sibling — Circle against Square — is false, because siblings share an ancestor rather than a lineage.

It never raises for the first argument. Anything that is not an instance of the class simply answers false, including values that are not instances at all:

class Shape {}

echo instance_of(42, Shape)
echo instance_of('circle', Shape)
echo instance_of(nil, Shape)
echo instance_of([1, 2], Shape)
false
false
false
false

That makes it safe to use as a guard on a value you know nothing about; no is_instance() check has to come first.

A class is not an instance of itself. Passing the class rather than an object gives false:

class Shape {}
class Circle < Shape {}

echo instance_of(Circle, Shape)
echo instance_of(Circle(), Shape)
false
true

This trips people up when a variable might hold either. Circle is a class; Circle() is an instance; only the second is “a Shape”.

The second argument must be a class, and here it does raise:

class Shape {}

catch {
  instance_of(Shape(), 'Shape')
} as e {
  echo e.message
}
instance_of() expects argument 2 to be a class, got string

The class name as a string is the common version of this mistake. Pass the class itself.

A class held in a variable works, because the check is on the value rather than on the spelling:

class Base {}
class Derived < Base {}

var Alias = Base

echo instance_of(Derived(), Alias)
true

So does a class reached through a module:

import set

echo instance_of(set.set([1, 2]), set.Set)
true

It Is How the Error Hierarchy Works

Every built-in error inherits from Error, so instance_of() is what lets one handler sort them:

echo instance_of(ValueError('bad'), Error)
echo instance_of(ValueError('bad'), TypeError)
true
false

That is the mechanism behind the “handle one kind, re-raise the rest” pattern in Error Handling, and behind mapping a domain error to an HTTP status in Chapter 25.

typeof(value)

typeof() asks a different question: “what exactly is this?”. On an instance it names the concrete class, and it never mentions ancestors:

class Shape {}
class Circle < Shape {}

echo typeof(Circle())
echo typeof(Shape())
echo typeof(Circle)
echo typeof(42)
echo typeof('text')
Circle
Shape
class
number
string

Note the third line. typeof() on the class itself answers class, not Circle — the class is a value of kind “class”, and its name is not what typeof() reports.

Which to Use

QuestionUse
can I treat this as a Shape?instance_of(x, Shape)
which exact class is this?typeof(x)
is this any instance at all?is_instance(x)

Reach for instance_of() by default. It is the one that respects inheritance, and code written with typeof(x) == 'Circle' breaks the day someone subclasses Circle — the subclass is a perfectly good Circle, and the string comparison says otherwise.

A Type Annotation Is the Same Check

Annotating a parameter with a class name performs exactly the instance_of() test, at every call, with a better error message:

class Shape {}
class Circle < Shape {}

def describe(shape: Shape) {
  return 'a shape of type ${typeof(shape)}'
}

echo describe(Circle())
echo describe(Shape())

catch {
  describe(42)
} as e {
  echo e.message
}
a shape of type Circle
a shape of type Shape
describe() expects parameter 'shape' (argument 1) to be a Shape, got number

Prefer the annotation when the answer decides whether the function should run at all, and instance_of() when the answer decides which branch to take.