We were fortunate that there is a simple way to implement a conslist structure within Java---we used null to mimick the Nil-structure and new Cell(...) to mimick the Cons-structure. But this simple approach won't always work with more complex structures, so we require a general strategy:
Say that we have an inductive definition of a type, T, that has n distinct clauses:
public abstract class T { ... }
public class Clause_i extends T { ... // private fields that remember the components contained // in a Clause-i structure ... // methods that return as answers the values in the fields }It is crucial that Clause_i extends T, because this means that new Clause_i(...) objects belong to data type T.
Recall this definition of the inductive type for conslists:
We must write three Java classes:
public abstract class ConsList // this names the data type { ... } public class Cons extends ConsList // this is used to construct Cons-objects { ...holds fields for a value part (``h'') and a next part (``t'') ... ...holds methods that return the value (``h'') and next (``t'') as needed... } public class Nil extends ConsList {... } // holds no fieldsHere are some Java codings of conslists with these definitions:
ConsList emptylist = new Nil(); ConsList clist = new Cons("c", emptylist); ConsList alist = new Cons("a", new Cons("b", clist));This looks pretty, because we have a distinct name, ConsList, that denotes all possible forms of conslists. Also, new Nil() clearly constructs a 0-sized list. (Contrast this with null, which means ``no value at all''.)
Here is the first version of our coding of the conslist data structure:
/** ConsList defines the data type of cons-lists: * a Nil cons-list will be modelled by new Nil(); * a Cons cons-list will be modelled by new Cons(h, t). */ public abstract class ConsList {} /** Cons defines a nonempty ConsList, containing a head and a tail. */ public class Cons extends ConsList { private Object hd; // the element saved at the front of the list private ConsList tl; // the remainder of the list /** Cons creates a new list whose front element is x and remainder is y */ public Cons(Object x, ConsList y) { hd = x; tl = y; } /** head returns the element at the front of this list */ public Object head() { return hd; } /** tail returns the remainder of the list */ public ConsList tail() { return tl; } } /** Nil defines an empty ConsList */ public class Nil extends ConsList { public Nil() {} }The resemblance of class Cons to class Cell is striking.
With this version of conslist, the Java coding of the lengthOf method looks like this:
/** lengthOf calculates the number of cells in a linked list * @param l - the leading cell of the list * @return the length of the linked list */ public int lengthOf(ConsList l) { int length = 0; if ( l instanceof Nil ) { length = 0; } else // l instanceof Cons must be true: { Conslist t = ((Cons)l).tail(); // extract sublist from l length = 1 + lengthOf(t); } return length; }We use Java's built-in instanceof operation to ask data structure l whether it is a Nil-structure. If it is, the answer is zero; if it isn't, we must use a recursion to count the lengthOf the sublist that is embedded within l.
Within the else-clause of the method, notice the casting in this statement:
Conslist t = ((Cons)l).tail(); // extract sublist from lThese castings are an annoyance, and we can avoid them by a clever coding trick in our definition of class ConsList. We see this later.
Remember these two slogans:
In contrast, an abstract class names a collection of classes that belong together with the same ``family name.'' These classes definitely live in the same package.
The slogans tell us that an abstract class is the correct construction for naming an inductively defined data type. Also, because we use an abstract class, we can insert, into the abstract class itself, codings for the some of the methods required of the structures that belong to the data type. (See the next section.)
/** ConsList defines the data type of cons-lists: * a Nil cons-list will be modelled by new Nil(); * a Cons cons-list will be modelled by new Cons(h, t). */ public abstract class ConsList { /** head returns the element at the front of this list * @return the head element, if it exists * @throw RuntimeException, if the list is empty and has no head */ public Object head() // here is the default coding of this method: { throw new RuntimeException("ConsList: head error"); } /** tail returns the remainder of the list * @return the list's tail, if it exists * @throw RuntimeException, if the list is empty and has no tail */ public ConsList tail() // here is the default coding of this method: { throw new RuntimeException("ConsList: tail error"); } }Now, when we write:
package ConsList; /** Nil defines an empty ConsList */ public class Nil extends ConsList { public Nil() {} }and when we code:
ConsList emptylist = new Nil();This gives the emptylist, a Nil object, two methods, head and tail. It means that we can use them:
... emptylist.head()...Of course, the result is an exception!
Now, we cleverly code class Cons like this:
package ConsList; /** Cons defines a nonempty ConsList, containing a head and a tail. */ public class Cons extends ConsList { private Object hd; // the element saved at the front of the list private ConsList tl; // the remainder of the list /** Cons creates a new list whose front element is x and remainder is y */ public Cons(Object x, ConsList y) { hd = x; tl = y; } public Object head() { return hd; } public ConsList tail() { return tl; } }Notice that we write new versions of head and tail. (This is called overriding the old definitions by the new ones.) Thus, when we code:
ConsList alist = new Cons("a", emptylist);we can also do
...alist.head()...to extract the object held in alist.
Here is the revised coding of lengthOf:
/** lengthOf calculates the number of cells in a linked list * @param l - the leading cell of the list * @return the length of the linked list */ public int lengthOf(ConsList l) { int length = 0; if ( l instanceof Nil ) { length = 0; } else // l instanceof Cons must be true: { length = 1 + lengthOf(l.tail()); } return length; }The cast is no longer needed to make the Java compiler happy.
Here are two more examples of methods:
public String toString(ConsList l) { String answer; if ( l instanceof Nil ) { answer = ""; } else { answer = l.head().toString() + " " + toString(l.tail()); } return answer; }
public ConsList append(ConsList list1, ConsList list2) { ConsList answer; if ( list1 instanceof Nil ) { answer = list2; } else { answer = new Cons(list1.head(), append(list1.tail(), list2)); } return answer; }With these techniques in hand, we are ready to do serious programming with recursively defined data structures.