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