Notes on writing a world-state viewer

Author: Jeff Dalton

Updated: Tue Feb 22 02:28:12 2005 by Jeff Dalton


1. What does the viewer do?

A world-state viewer is responsible for displaying the panel's
model of the current state of the world.  The state is a set of
pattern-value pairs.  For example, using one possible textual
notation:

   colour block-1 = red
   size block-1 = small
   position block-2 = (10 30)
   position robot = (100 10)

To the left of the "=" is the "pattern", to the right, the "value".
This is mere terminology.

The default value is "true".  Anything not specified in the state is
unknown.  There is no closed-world assumption that says everything not
specified is false.

The viewer is told about changes in the state.  It isn't (in 2.2) ever
given a whole state to display, although the first change it sees will
be the entire state at that time.  This means that most world-state
viewers will have to maintain their own record of the whole state.

The viewer must also be able to "reset" - to clear any record or
display of what the state is.


2. How do I change the viewer used by I-P2?

Define a subclass of ix.ip2.Ip2 that redefines the following method:

    /**
     * Called to create the state viewer.  This method can be
     * redefined in subclasses that want to instantiate a different
     * viewer class.
     */
    protected StateViewer makeStateViewer() {
        return new StateViewTable(this);
    }

Instantiate whatever viewer class you desire instead of StateViewTable.


3. How do I define a new viewer class?

The class ix.ip2.StateViewTable may be used as an example.  It
corresponds to file java/ix/ip2/StateViewTable.java in the I-X
distribution.

The class must:

  * implement the StateViewer interface;

  * be a subclass of java.awt.Component;

  * have a 1-argument constructor that takes an instance of Ip2 as a
    parameter.  (In the constructor declaration, a superclass of Ip2,
    such as IXAgent, may be specified instead.)

In I-X 2.2, the StateViewer interface extends the ProcessStatusListener
interface.  This is may change.  Until it does, however, you must
define some methods that will never actually be called.  Here are
the ones that WILL be called:

    public void reset();

    public void stateChange(ProcessStatusEvent e, Map delta);

The ones that will not be called are:

    public void statusUpdate(ProcessStatusEvent e);

    public void newBindings(ProcessStatusEvent e, Map bindings);

Here is an outline viewer based on the above:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
package ix.ip2;         // or whatever package you desire

import java.util.*;
import javax.swing.*;
import ix.icore.process.event.*;
import ix.util.*;
import ix.util.lisp.*;

public class MyStateViewer extends JPanel implements StateViewer {

    /**
     * Constructs a viewer for the indicated agent.
     */
    public MyStateViewer(IXAgent ip2) {
        super();
    }

    /**
     * Sets the viewer back to something approximating its initial state.
     */
    public void reset() {
        // Clear state data    
    }

    /*
     * Methods in ProcessStatusListener interface
     */

    /** Ignored by this viewer. */
    public void statusUpdate(ProcessStatusEvent event) { }

    /** Ignored by this viewer. */
    public void newBindings(ProcessStatusEvent event, Map bindings) { }

    public void stateChange(ProcessStatusEvent event, Map delta) {
        for (Iterator i = delta.entrySet().iterator(); i.hasNext();) {
            Map.Entry e = (Map.Entry)i.next();
            LList pattern = (LList)e.getKey();
            Object value = e.getValue();
            recordNewValue(pattern, value);
        }
    }

    protected void recordNewValue(LList pattern, Object value) {
        Debug.noteln("State viewer sees " + pattern + " = " + value);
        // ...
    }

}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


4. How are patterns and values represented?

As you can see in the code above, a pattern is an LList and a value
can be any Object.  An LList is a class of singly-linked, "Lisp-like"
lists.  In the viewer, you can usually ignore the fact that that
particular class is used and use the java.util interface List in
declarations instead.  But when constructing a pattern, or a
pattern-element that is a List, an LList must be used.

Although values and pattern-elements can be any Object, they will in
practice always come from a restricted set of classes:

  * Symbols - instances of ix.util.lisp.Symbol.
  * Strings - instances of the class String.
  * Numbers - instances of classes Long, Double, etc.
  * Variables - instances of ix.util.lisp.ItemVar.
  * Lists - instances of ix.util.lisp.LList

Variables will not appear in any pattern or value in the world state.

To create a Symbol, call Symbol.intern(String name).  If a Symbol
of that name already exists, it is returned; otherwise a new Symbol
is constructed.  This ensures that there is only one Symbol per
name and hence that they can be compared with ==.

For example:

   static final Symbol S_REQUEST = Symbol.intern("request");

   ...

      if (pattern.get(0) == S_REQUEST) ...

An ItemVar is obtained by using a name that begins with "?".

There are a number of ways to construct LLists.  Here are some of
the most common:

  Lisp.NIL                       // the empty LList
  Lisp.cons(Object, LList)       // for recursive construction
  Lisp.list(Object)
  Lisp.list(Object, Object)
  Lisp.list(Object, Object, Object)
  Lisp.list(Object, Object, Object, Object)
  ... up to 5 arguments ...

  LList.newLList(Collection)

  N.B. new LList(Collection) does NOT work.

  (LList)Lisp.readFromString(String),
      where the String is a list in Lisp-like syntax.

The Lisp class is ix.util.lisp.Lisp.

You can also match against patterns (treated as data) by constructing
a pattern that contains ItemVars.  For example:

  import ix.util.lisp.*;
  import ix.util.match.*;

  static final Symbol
      Q_AGENT = Symbol.intern("?agent"),
      Q_CONTENTS = Symbol.intern("?contents");

  LList syntax = Lisp.readFromString("(send_to ?agent ?contents)");

  LList data = ...;

  MatchEnv env = SimpleMatcher.match(syntax, data);

  ...
  if (env != null) {
      Symbol agentName = (Symbol)env.get(Q_AGENT);
      Object messageContents = env.get(Q_CONTENTS);
      ...
  }

There are more spohisticated ways to do this kind of thing, but they
are "beyond the scope of this document" (as they say).
