4 Data Structures
Pyret makes it easy to define new kinds of data. For instance, suppose
we have recently taken on a job as a zoo-keeper, and need to maintain
inventories of animals. First, we might make a new datatype to
represent the different kinds of animals:
data Animal: |
| boa(name, length) |
| armadillo(name, is-alive) |
end |
This says that there are two kinds of animals, boas and
armadillos. Each has two additional pieces of information,
known as fields: a name and length in one case, and a name and
living status in the other.Armadillos have a high
mortality rate. As the old political saying goes, “In Texas there
ain’t nothing in the middle of the road except a yellow line and a
dead armadillo”, attributed to Jim Hightower and others.
First let’s see how to make instances of these data. We simply use the
name given in the data definition as the name of a function, with as
many arguments as the data definition indicates:
s1 = boa("Slithers", 10) |
s2 = boa("Monty", 5) |
s3 = boa("Feathered", 15) |
a1 = armadillo("Houston", true) |
a2 = armadillo("Austin", false) |
Each of these functions is called a
constructor.Thus boa is a constrictor
constructor.
Now we can write functions over these data. Let’s implement the Texan
highway function, which renders armadillos defunct. The critical thing
we need is a way of telling which kind of Animal we have, and
extracting its fields. Here is one way:
fun run-over-1(a): |
cases (Animal) a: |
| boa(_, _) => raise("doesn't apply to boas") |
| armadillo(n, _) => armadillo(n, false) |
end |
where: |
run-over-1(a1) is armadillo("Houston", false) |
run-over-1(a2) is armadillo("Austin", false) |
run-over-1(s1) raises "" |
end |
There is actually another way to extract fields: using the
dot-operator. Here it is at work:
fun run-over-2(a): |
armadillo(a.name, false) |
end |
However, this version is dangerous, because it extracts the
name field of every argument, irrespective of whether or not
it’s a boa! Thus, these tests pass:
run-over-2(a1) is armadillo("Houston", false) |
run-over-2(a2) is armadillo("Austin", false) |
but so does this one:
run-over-2(s1) is armadillo("Slithers", false) # WRONG! |
Therefore, we should use direct access to fields carefully, only after
making sure we’re working with the kind of data we expect.
There is actually one way to use the dot operator safely in this
setting, which is to write an annotation in the function header
indicating what kind of data we expect. However, the annotation
a :: Animal is too weak, because we’re trying to avoid
boas.
Pyret allows you to refine an annotations, and datatypes provide
predicates to identify the different kinds of constructed data,
by prefixing the constructor name with is-. For instance,
is-armadillo is true only of values constructed with
armadillo, not boa or anything else. Thus, the
annotation a :: Animal(is-armadillo) says that a can
only be bound to Animal values that further pass the
is-armadillo predicate:
fun run-over-3(a :: Animal(is-armadillo)): |
armadillo(a.name, false) |
where: |
run-over-3(a1) is armadillo("Houston", false) |
run-over-3(a2) is armadillo("Austin", false) |
run-over-3(s1) raises "" |
end |