Rice University - Comp 212 - Intermediate Programming

Spring 2004

Lecture # 05- Immutable List Structure

 

Classic List Definition

The following is a common definition of a list.

A list is either empty or non-empty.  If it is empty it contains no elements.   If is not empty, it contains an element called first and an element called rest, which is itself a list.

Object-Oriented List Formulation

We rephrase the above classic list definition as follows.

There is an abstract notion of a list.
An empty list is a list.  It has no element.
A non-empty list is a list.  It has an element called first and an element called rest, which is itself another list.

We model the above abstract formulation of a list using three interfaces: IList, IEmptyList, and INEList.  The table below shows the differences between the object-oriented formulation in Java and the "data-directed" formulation in Scheme (a la Comp 210).

 

OO Design in Java Data-directed Design in Scheme
/**
 * Represents the abstract notion of a list.
 * At this point, it has no behavior, i.e. no method.
 * We shall add behaviors later.
 */
public interface IList {
}

/**
 * Represents the immutable empty list, which is a list.
 * At this point, the empty list has no behavior.
 * Note that in contrast with the non-empty list, since it has
 * no first and no rest, there is no getFirst() nor getRest() method.
 */
public interface IEmptyList extends IList {
}

/**
 * Represents the immutable non-empty list, which is a list.
 * A non-empty list has a data object called first, and an
 * isomorphic subcomponent called rest.  Its structural behavior
 * provides access to its internal data (first) and substructure (rest).
 */
public interface INEList extends IList {
    /**
     * "Gettor" method for the list's first.
     * @return this INElist's first element.
     */
    public abstract Object getFirst();

    /**
     * "Gettor" method for the list's rest.
     * @return this INElist's rest.
     */
    public abstract IList getRest();
}

;; A list is either
;;   (make-Empty) , the empty list
;; or
;; (make-NonEmpty f r) where
;; f is a data object and r is a list

 

The above is only an abstract specification of the (immutable) list structure.  Note that the Scheme formulation is written as comments, whereas the Java formulation is written as actual Java code.  The Java code specifies in effect a "contract" of what the list structure "looks like" and what it "behaves" like (i.e. what it is capable of doing).  

In order to create concrete IList objects and work with them, we need to define concrete classes to implement the above interfaces.  Below is an example of concrete list implementations.

 

/**
 * Concrete implementation of the empty list interface, IEmptyList.
 * Since the empty list has no fields, there is no need to explicitly
 * define any constructor for EmptyList.
 * However, the compiler will generate the following constructor:
 * public EmptyList() {};
 * Note that there is no code inside the curly braces.
 * The above constructor has no parameter.  A constructor without any
 * parameter is called a "default" constructor.
 * To instantiate a concrete empty list, we simply call: new EmptyList();
 */
public class EmptyList implements IEmptyList {
}

(define-struct Empty())
;; make-Empty is automatically generated


/**
 * Concrete implementation of the non-empty list interface, INEList.
 * Contains two pieces of data:
 *   first: an Object representing the first data element
 *   rest: an IList object representing the rest of this non-emptylist.
 * When a class contains other objects that are isomorphic to itself,
 * this class is called a composite.
 * Provides concrete code for 
 * a constructor to initialize this NEList to a given first and rest,
 * the getFirst() method for accessing the first data element,
 * getRest() method for accesssing the rest of the list.
 */
public class NEList implements INEList {

    /**
     * The first data element of this NEList.
     */
    private Object _first;
    /**
     * The rest or "tail" of this NEList.
     * Data Invariant: _rest != null;
     */
    private IList _rest;

    /**
     * Initializes this NEList to a given first and rest.
     * @param first the first data element of this NEList.
     * @param tail != null, the rest of this NEList.
     */
    public NEList(Object first, IList tail) {
        _first = first;
        _rest = tail;
    }

    /**
     * Returns the first data element of this NEList.
     */
    public Object getFirst() {
        return _first;
    }

    /**
     * Returns the rest of this NEList.
     */
    public IList getRest() {
        return _rest;
    }
}

(define-struct NEList(first rest))
;; make-NEList,  NEList-first, NEList-rest

;; are automatically generated

 

The following UML diagram illustrates the above object-oriented design (click on the diagram to see the full documentation).  

The above design allows us to instantiate a list of any size.

IList list0 = new EmptyList(); // the list ()

IList list1 = new NEList("one", list0); // the list ("one")

IList list2 = new NEList("two", list1); // the list ("two" "one")

IList list3 = new NEList("a", new NEList("b", new NEList("c", new EmptyList()))); // the list ("a" "b" "c")

How do we write code to operate (i.e perform algorithms) on the list?  For example, how do we compute the length of a list?  The following table shows the Scheme code and a direct translation into Java.  The Java translation is not object-oriented since it makes use of the instanceof operator to check for the type of the list instead of making use of the polymorphic nature of IList.  (We show the object-oriented version later).

Java Code Scheme Code

/**
 * A non object-oriented way of coding list operations.
 * Scheme functions are translated into public static methods.
 * The instanceof operator is used to check for the type of a
 * given list.
*/
public class ListOps {
    /**
     * Computes the length of a list.
     * @param L one of the two types of IList.
     */
    public static int GetLength(IList L) {
        if (L instanceof IEmptyList) {
            return 0;
        }
        else {
            return 1 + GetLength(((INEList)L).getRest());
            // Note how type-casting is used here!
            // Why do we need to type-cast?
        }
    }
}

;; Contract:
;; getLength: list -> number
;; Purpose:
;; (getLength alist) returns the number of elements in alist.
(define (getLength alist)
  (cond
    [(Empty? alist) 0]
    [(NEList? alist) (+ 1 (getLength (NEList-rest alist)))]))
 
 
Exercise: Write a (non object-oriented) method for ListOps to append two lists together.
 
dxnguyen@cs.rice.eduAlan Cox, last revised 09/03/03