Notes on writing a world-state viewer

Author: Jeff Dalton

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.

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.

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:

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);
        // ...
    }

}

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:

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


Jeff Dalton <J.Dalton@ed.ac.uk>