How to Implement Conslists and Other Inductively Defined Data Types

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:

An object is a T-object if
We implement T in Java as follows:
  1. Write an abstract class T that merely names the new data type. (We don't build objects from class T.)
    public abstract class T
    { ... }
    
  2. For each Clause-i of the inductive type definition, write a class, class Clause_i, that will be used to construct objects of the format defined by Clause-i:
    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.
We want the abstract class to give an overall name to the data type.

(Re)Implementing ConsLists

To understand the above methodology, let's reimplement conslists.

Recall this definition of the inductive type for conslists:

An object is a ConsList-object if
Rather than crudely implement this definition with null and class Cell, let's follow the general strategy:

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 fields 
Here 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  l
These castings are an annoyance, and we can avoid them by a clever coding trick in our definition of class ConsList. We see this later.

Why an abstract class? Why not an interface?

We coded class ConsList so that it is an abstract class. Why did we do this, rather than write public interface ConsList?

Remember these two slogans:

  1. An interface defines a connection point so that classes can connect together.
  2. An abstract class defines a name for a collection of classes that belong in the same family.
We use an interface, like public interface ActionListener or public interface Key, to name the methods that a class must have to ``connect'' to another class. The classes that implement the interface are unrelated, and almost certainly live in different packages.

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.)

Another implementation of ConsList that doesn't use casts

Here is a clever trick that eliminates the use of all casts when we work with conslists. The idea is to provide ``default'' codings of all the possible operations we require when we work with conslists:
/** 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.


A package that holds the implementation of conslists

Example application: an implementation of sets using ConsLists