Traits#
Types form hierarchies which allow us to classify data—but we get only a single hierarchy. Say we’re modelling a small game where the player can use different tools to interact with the world. We could come up with an hierarchy for these tools:
abstract tool;
abstract mining-tool is tool;
type shovel is mining-tool;
type pick-axe is mining-tool;
abstract gardening-tool is tool;
type shears is gardening-tool;
type watering-can is gardening-tool;
But here we have a problem. We’ve said that shovel
is a mining-tool
,
but a shovel would certainly be useful in gardening too! It could help us
dig the soil and transplant little buds. The problem with describing the
world in terms of Crochet hierarchies is that we can only have one. We
can’t have different perspectives on a particular type. They have one
inherent classification and that’s it. This is intentional.
To overcome this limitation, Crochet allows you to model things in terms of what they do as well. So instead of thinking of what kind of tool each of these are, we can think about what these tools can be used for.
Our revised hierarchy looks like such:
abstract tool;
type shovel is tool;
type pick-axe is tool;
type shears is tool;
type watering-can is tool;
We then complement this with the idea of traits. We can only describe doing things in Crochet through commands, so a trait is essentially a list of requirements on which commands must be defined for something to be classified that way.
For example, if we were to have a mining
trait, it could look like this:
trait mining with
command Tool mine: (Block is block);
end
This means that, in order to be a mining tool, we must be able to use the
command Tool mine: block
with it. We could define those for our game:
command shovel mine: (Block is sand-block) = ...; // more effective
command shovel mine: (Block is rock-block) = ...; // less effective
command pick-axe mine: (Block is sand-block) = ...; // less effective
command pick-axe mine: (Block is rock-block) = ...; // more effective
And we also have to tell Crochet that the provision of these commands is
intentional. We’re defining them because we want these tools to be
a mining tool. We achieve this by using the implement
declaration:
implement mining for shovel;
implement mining for pick-axe;
With this it’s also fair game for our shovel to be a gardening tool. But “gardening” is too broad of a category here. We can use the shovel to dig up holes so we can move or transplant things, but it doesn’t really make sense to use the shovel to water plants. So we have to break down this category a bit to the specific uses that we expect:
trait transplanting with
command Tool dig-hole: (Block is block);
command Tool dig-out: (Plant is plant);
end
trait watering with
command Tool water: (Plant is plant);
end
This way we can implement transplanting
for shovels and watering
for the watering can.
Intentionality of implementation#
Crochet requires you to not only define all of the commands a trait requires, but also to tell it “yeah, I’m doing this because I want to implement this trait”. Shouldn’t that be obvious?
Sadly, no. And that’s only partly due to how Crochet’s commands work.
First, commands can be defined anywhere, by anyone. And it’s quite
possible that someone may already have defined a command _ water: _
that happens to, accidentally, work for our tools. It would be bad
to consider that to be an intention of being a watering
tool,
only for the command to do something unexpected.
But a slightly more subtle issue is with the human expectations of a trait. When we come up with a trait we’ll have a whole set of expectations in mind for our little world, but not all of those expectations are—or can be—captured in the trait declaration.
For example, consider the trait equality
in Crochet’s standard
library. It’s defined as this:
trait equality with
command X === Y;
end
That’s it, just define a _ === _ command and you’re golden. What
this declaration does not tell us, however, is that there’s an
expectation that the _ === _ command works commutatively—that is,
if A === B
, then it must also be the case that B === A
. We
can swap the values around and still get the same results back.
It would be quite awkward if, I was given a decorated box and a plain box, and I asked Crochet: “Hey, are the contents of the decorated box the same as the contents of the plain box?”, and the system replied, “Yes, they are!”. But then when I ask, “And are the contents of the plain box the same as the contents of the decorated box?” it replied, “No, I’m afraid they aren’t”.
Requiring people to be intentional about traits means that we can have a bit more of confidence that these expectations which cannot be captured in Crochet’s code will still be meaningful.
Trait relationships#
Traits do not form hierarchies, in the way types do, however they can relate to other traits. This happens when, in order to something to exhibit a particular trait, then it must be the case that they also exhibit another trait.
For example, the total-ordering
trait in Crochet’s standard library
allows us to ask if something is smaller, bigger, or equal to another
thing. It’s defined for numbers, so we can ask questions like, “Is
1 less than 2?” (which in Crochet notation would be 1 < 2
). We could
define this trait like this:
trait total-ordering with
command A < B;
command A === B;
command A > B;
end
But the standard library, instead, defines it this way:
trait total-ordering with
requires trait equality;
command A < B;
command A > B;
end
The requires trait equality
declaration tells Crochet that, in order
for something to have a total ordering, it must also have some concept
of equality—the equality trait requires the _ === _
command to be
defined.
Again, the reason to make these relationships is that traits will generally
have some implicit assumptions—some expectations that aren’t communicated
in the declaration itself. In the case of equality this expectation is that
if I can say A === B
, then I must also be able to say B === A
and
get the same answer back. It has to be commutative.
In the case of total ordering, there’s an expectation that for any two values,
we either have A < B
, A === B
, or A > B
. But we cannot have A < B
and A > B
be true at the same time—it doesn’t make sense for something
to, at the same time, be smaller and bigger than some other thing. This is
what makes the ordering total, in fact.
In this case, the type must declare that it implements both total-ordering
and equality
—Crochet does not derive that knowledge from the relationship
we’ve established. And the reason for this is, again, that these command
definitions and implement declarations can happen anywhere. Making them
happen implicitly sometimes could be confusing.
Implementing multiple traits#
Unlike type hierarchies, we can actually implement multiple traits for a specific type. That’s indeed what they’re for. We can see a type through different lenses based on what we want to use them for.
For example, the integer type in the standard library implements the
arithmetic trait—we can use arithmetic operators like _ + _
and _ - _
with them; the equality trait—we can use _ === _
; the total ordering
trait—we can use _ < _
and _ > _
; and a few others.
In the first example of this section, where we talked about modelling tools for mining and gardening in a game, there were many types of mining tools—the shovel being one of them—, but many tools could also be used for multiple things. We could use a shovel as a mining tool or a gardening tool.
Testing for traits#
We can test types with the is
operator: Tool is shovel
answers
whether some Tool value is of type shovel
. We have a similar test for
traits: Tool has mining
—which we can read as “Tool has an implementation
of the mining
trait”—is how we test for traits.
Because a type can implement multiple traits, our trait tests can also
have multiple trait requirements. For example, Tool has mining, transplanting
will succeed if the Tool has both an implementation of the mining
trait and
an implementation of the transplanting
trait.
Trait tests are not transitive—they do not follow trait relationships. What this means is that if we test for a trait that’s required by another trait, then it would not succeed if the type only declares an implementation on the more encompassing trait.
In a less abstract way, if we consider that the total-ordering
trait
requires us to implement equality
as well, and we go on to write the
following definitions:
enum battery = full, half-discharged, almost-gone, gone;
implement total-ordering for battery;
Then a test for the implementation of total ordering, like
full has total-ordering
, would succeed. But a test for the
implementation of equality, like full has equality
, would not.
Even though we may argue that, if something has a total ordering, then
it must also have an equality.
Crochet does not prevent you from running programs that don’t fulfill all of the specified trait requirements.
Traits in a type hierarchy#
While trait relationships are not followed, things are a bit more complicated with types. If a type higher in the hierarchy declares that it implements a certain trait, then all types below it must also implement that trait.
For example, consider modelling a life simulation game. We could come up with the following types and traits:
type being;
type cat is being;
type ragdoll is cat;
trait eating with
command Being eat: Food;
end
If we say that being implements the eating trait, then the cat and ragdoll types must all implement the eating trait too, but they do not need to declare again that they implement it.
That is, we can have the following implementations:
implement eating for being;
command ragdoll eat: (Food is cat-food) = ...;
And Crochet would still consider that ragdoll
has the eating
trait
when testing for it, despite not having a more specific declaration of
such.