Skip to content

Protocols

Protocols

A collection of methods that more than one class implements is called a protocol. For example, all standard classes permit cloning by implementing a method called clone. Some classes support addition and other arithmetical operations by implementing methods called add, sub etc. Protocols are a major feature in dynamic languages, because they enable code to work with many kinds of objects regardless of their inheritance hierarchy. A rather trivial function that scales its argument, for example,

fn scale(x) {
  return 2*x
}

immediately works with Integers, Floats, Matrices, Sparse matrices, Complex numbers, for example, and will also work with any future object that implements a mul method.

Morpho protocols

Morpho's standard collection of types implement a number of protocols as we describe here.

Clone

Objects that can be cloned provide a method called clone that makes a copy of the object. Typically, this is a shallow copy, namely the object itself is cloned by the contents are merely copied (List is a good example). Since most objects inherit from Object, this method is provided by default.

Count

The count method returns the number of constituent values included in a collection. A List returns its length, a Dictionary returns the number of key/value pairs, a Matrix returns the number of entries, etc.

Enumerate

The enumerate method enables a collection to participate in for ... in loops. The loop code calls enumerate repeatedly with a single integer value indicating the requested entry; the collection should return the corresponding value. If a negative value is supplied, the collection object should return the total number of entries in the collection.

Accessing collections

Collection objects may provide two methods that facilitate access to the collection:

  • index(i) Retrieves the value corresponding to the index i. The index method can be defined with any number of parameters, or using variadic parameters, to support multi-dimensional collections. The programmer must supply the correct number of indices when the collection is accessed or an ArrayDim is thrown.

  • setindex(i, val) Sets the value in the collection corresponding to index i to be val. As for index, setindex can be defined with more than one index parameter; the value to be set is always the last parameter.

Retrieving information from a collection using the syntax a[1,2] is translated into a call a.index(1,2) at runtime; similarly setting a value a[1,2]=2 is translated into a.setindex(1,2,2).

Arithmetic

Objects may support arithmetic operations. When code like

var a = b + c

is encountered, Morpho first checks if it knows how to perform the operation. If not, it checks to see if the left hand operand, b, provides a method called add. If it does, the addition is redirected to b's add method using c as the argument

var a = b.add(c)

If b doesn't provide an add method, Morpho checks to see if c provides an addr method. If so, this is called with b as the argument

var a = c.addr(b)

Analogous methods sub, mul, div and "right associated" versions subr, mulr, divr handle subtraction, multiplication and division respectively and allow the programmer to support non-commutative algebras.

Defining new protocols

Some languages provide a specific protocol keyword to define protocols. To keep things simple, Morpho doesn't do this but you can achieve much the same effect with classes and multiple inheritance. This class defines a protocol for objects that are cookable, i.e. respond to a cook method

class Cookable {
  cook() { }
}

Now let's define a few objects that implement this protocol. Kale gets most of its properties from its Plant parent class (not shown), but is also cookable:

class Kale is Plant with Cookable {
  cook() { print "It wilts deliciously!" }
}

So is this Cake, which in the absence of any other parent classes simply inherits directly from Cookable:

class Cake is Cookable {
  cook() { print "Cooked to perfection!" }
}

It's often helpful to restrict a function or method to accept only objects that implement a particular protocol, which can be done with a type annotation on the relevant parameter. Here's a function that makes dinner, and accepts only Cookable objects:

fn dinner(Cookable w) {
  print "Making dinner"
  w.cook()
}