Hierarchies of types#

Types in Crochet are organised into a hierarchy. This hierarchy is described by the way types are defined.

For example:

type being;

type person is being;
type alissa is person;

type cat is being;
type caramel is cat;

These types describes a taxonomy—a classification of different kinds of things into a hierarchy. From this classification we know that caramel is a cat. But also, transitively, caramel is a being—because all cats are also beings. Likewise, alissa is a person and also a being.

All of them are also, implicitly, “any”—because that’s the root of Crochet’s hierarchy. When we talk about “any”thing, then it makes sense that we could possibly be talking about any of these types.

We can visualise this hierarchy like so:

+ any
|
`--+ being
   |
   `--+ person
   |  |
   |  `--o alissa
   |
   `--+ cat
      |
      `--o caramel

Modelling possibilities#

Hierarchies are used for Dispatch, but they’re also a tool for reasoning about programs and designing programs in Crochet. One such design approach is to use types to capture the possibilities in the little world we’re creating in our programs.

For example, suppose we’re writing a program that simulates flipping a coin. We toss the coin up, and when it falls it’s going to give us some result. We can capture this result as a possibility:

type coin-result;

Now, there are really only two results we can get: when the coin falls, it’ll land with one or the other side up. These sides are generally called head and tail, so we can model these as discrete possibilities of a coin result:

type head is coin-result;
type tail is coin-result;

Of course, there’s still a remote possibility that, if we were to more accurately capture physics and complex environments, the coin never lands on either side! In that case, there’s no real result and we should flip it again. But it’s still interesting to capture this as a possibility:

type undecided is coin-result;

By capturing this as a hierarchy not only do we get to see what are the things that can happen when we flip a coin, and talk about the specific results that we may get. It also lets us talk about results in general.

Caveats of a static hierarchy#

It’s important to note that Crochet admits only one static hierarchy. This means that the feature is a poor fit for contextual hierarchies.

For example, if we think about mathematical shapes, then a square would be just a special case of a rectangle, and we may proceed to define the following hierarchy:

type rectangle(width, height) is shape;
type square(side) is rectangle;

At first, this might make sense, but we run into cases like the following:

let A = new rectangle(10, 10);
let B = new square(10);

Now, both A and B are mathematically equivalent shapes—they’re both squares with sides of length 10. But Crochet’s type system does not know that a square means “all sides have equal length”, it only knows that rectangles have a width and height component, and squares, which are a kind of rectangle, only have a side component. Therefore the type system does not consider A to be a square—even though we, humans, do.

So, as a rule of thumb, it’s better to make subtypes only if they unconditionally fulfill all of the properties of its parent type. This is often described as the Liskov substution principle.

Caveats of an open hierarchy#

It’s important to note as well that hierarchies in Crochet are open. This means that new types may be added to the hierarchy at any point in time, by anyone.

For example, consider the case where one is modelling an RPG system where characters may be affected by different conditions. This will often be defined as an hierarchy, so we can talk about conditions in general, as well as specific conditions:

type condition;
type poisoned is condition;
type sleeping is condition;
type silenced is condition;

As it stands, the author of the condition type has thought of three different conditions: poisoned, sleeping, and silenced. It’s quite likely that the code dealing with conditions may end up baking assumptions about its specific conditions. However, there is nothing in Crochet that prevents some other piece of code from attaching more conditions to this hierarchy:

type petrified is condition;

If such a declaration appears at some later point, somewhere in the program, then petrified will be considered as much as a member of the condition hierarchy as any other. These declarations may, indeed, happen when the program is executing—through the Crochet interactive playground.

In order to add new types to the hierarchy, however, an author would need to have access to the condition type. So limiting the visibility of this type would allow more control over the hierarchy. But the open and extensible behaviour is often more desirable if you’re sharing your code with someone else.

Caveats of field projection#

Often programming languages that feature type hierarchies also have subtypes inherit the fields from the parent type. That is, given something like:

type rectangle(width, height);
type square(side) is rectangle;

Then, in common object-oriented languages, the square type would really define three fields: width, height, and side. Where the first two would be inherited from rectangle.

Crochet does not work that way. In Crochet, there is no field inheritance. The layout of a data structure is precisely what is specified in its declaration. Commands, however, are inherited, and thus it is important for inherited commands to not use field projection directly.

Testing classifications#

Once we have a hierarchy of types, it’s useful to ask the system questions like “is X a cat?”, where X can be any Crochet value. Crochet then allows the program to behave differently depending on the answer to that question.

For example, if we were to model a rock-paper-scissors classification:

abstract move;
singleton rock is move;
singleton paper is move;
singleton scissors is move;

Then we could determine the winning move of this game by testing for this classification:

condition
  when (A is rock) and (B is scissors) => "A wins";
  when (A is paper) and (B is rock) => "A wins";
  when (A is scissors) and (B is paper) => "A wins";

  when (B is rock) and (A is scissors) => "B wins";
  when (B is paper) and (A is rock) => "B wins";
  when (B is scissors) and (A is paper) => "B wins";

  otherwise => "It's a draw";
end

The Value is type expression allows us to ask Crochet questions about the classification of the value in the type hierarchy we’ve defined. This will be true whenever the value’s type is the same as the one we’re asking about, of course. But it’ll be also true if the type we’re testing is any of the parents of the value’s type. The value rock is of type rock, but also of type move, and also of type any.

The recommended way of testing for classifications in Crochet, however is to use commands. We can specify type requirements in commands, and new commands can be defined at any point later, if these need to be adapted or extended to a different context. With commands our example before would look like this:

command move beats: move = false;
command rock beats: scissors = true;
command paper beats: rock = true;
command scissors beats: paper = true;

command (A is move) play-against: (B is move) =
  condition
    when A beats: B => "A wins";
    when B beats: A => "B wins";
    otherwise => "It's a draw";
  end;

In move beats: move, we’re defining a base line for all moves. If we don’t know what specific move has been used, then we can’t truly say which one wins in the game. But we follow that by defining different commands for each specific type. rock beats: scissors captures one of these specific commands, where we override our base line with a more informed behaviour for this game.

Commands and dispatch are discussed in details in the commands chapter.