Scripting and Dynamic Interaction in Java

Michael Travers

Unpublished draft

Interactive programming environments for fully dynamic languages (such as Lisp and Smalltalk) allow developers to experiment with their programs through an interpreter or other interface while they are constructing them. This kind of activity, which I call dynamic interaction, helps support rapid prototyping and the use of the programming environment as a conversational medium. Dynamic interaction can be implemented in many ways, but at minimum allows a programmer to interactively create and manipulate program objects; to invoke arbitrary parts of the program; and to observe the results of program activity. Dynamic languages are not necessary to dynamic interaction, but make supporting it much easier than it would be otherwise. Such languages generally feature typed objects but untyped variables, late binding, incremental programming, and garbage collection. Environments for dynamic languages usually support the ability to modify code on the fly, the ability to recover flexibly from errors, and full interactive access to the live computational environment.

Languages that are based on a strictly compiled model tend not to support this level of interactivity. Java, while it absorbed some of the ideas of dynamic languages (i.e., garbage collection and self-describing objects), omitted others (such as untyped variables and a uniform object model). Among the left-out features was the ability to interact dynamically with the environment of a running program. This omission is probably due to the fact that Java is both strongly typed and by definition compiled to bytecode, facts which tend to work against interactive interpreters.

Scripting languages share many of the same requirements as dynamic interaction. They too are generally untyped, interpreted, and permit on-the-fly modification of code for rapid prototyping. These days, a great deal of software development takes the form of scripted components: that is, a combination of some prefabricated components and a scripting language for gluing them together into an application. In such a world, where components may be poorly documented black boxes, it seems particularly important to provide developers with the means to perform interactive experiments on them during the course of development.

In this paper I describe some efforts to provide scripting and dynamic interaction capabilities in the Java environment. I constructed a variety of tools that permit dynamic interaction, including a scripting language, a web-based interface, and a variety of graphic object inspectors. All of these interfaces run from within a standard Java VM, and can run alongside existing Java applications such as debugging and scripting tools.

Dynamic Interaction

Traditional programming environments provide only slow, narrow, and awkward channels between the developer and the system under development. Command-line debuggers are akin to command-mode text editors -- the user has to do a great deal of cognitive work to synchronize their mental model with what is going on inside their program. Graphic debuggers and steppers are better, but in general they only allow the execution of a program to be viewed, paused, or stepped -- the user cannot actually manipulate computational objects.

Direct manipulation tools such as WYSIWYG text editors and spreadsheets use interface technology to remove barriers between programmer and application objects. They allow the user to have the feeling of directly interacting with the system under development. This style of interaction is now the norm for end-user applications such as word processors and drawing programs, but is not yet available in most programming environments. Boxer [diSessa and Abelson 86], LiveWorld [Travers 96], and Self [Ungar and Smith 87] are some environments that do provide this kind of interface, which for the purposes of this paper I will call dynamic interaction.

Dynamic interaction is not quite the same as a dynamic language, but the notions are closely related and it is much easier to provide dynamic interaction for a language which is itself dynamic.

It can be useful to contrast the idea of dynamic interaction environments with some related techniques for integrating visual interfaces and programming. Visual builders and most visual programming languages are visual only "before the fact": they allow the user to construct portions of a program through an interactive graphic interface, but the program itself runs separately from the construction phase. Software visualization, in contrast, usually produces its graphic component "after the fact". A program runs and produces a trace which is then turned into a graphic display or animation.

Dynamic interaction, by contrast, involves a continuous interactive relationship with a programming environment. Graphical interface techniques can certainly help maintain this relationship, but are not essential to it.

Can Java support dynamic interaction?

Java is a "semi-dynamic" language, and it was not clear at the start of this project that it could support dynamic interaction at a meaningful level. As it turned out, Java's reflection API provides enough power to support activities such as interactive object creation and manipulation, although since it is relatively low-level it required an extra layer of code to provide an interface at the right level of abstraction, which makes up the Invoke class.

To make dynamic reflection useful, it requires not only the capability to do arbitrary runtime operations but a suitable user interface to this functionality. I built several different interfaces; the most developed (and the basis for the others) is an interpreter for an extended version of the Scheme language, called Skij. This provides a powerful text-based interface to dynamic interaction. A variety of other tools that used graphic or web-based interfaces were also constructed.

Scripting

A scripting language is a programming language which is designed to control or integrate other programs, which are usually independently developed in some other language. The various Unix shells are often used as scripting languages. Other examples of scripting languages are AWK, Perl, TCL, Rexx, and scsh (the Scheme shell). Scripting languages are usually interpreted, and have good facilities for invoking external programs and establishing connections between them.

Scripting languages are also used within single applications as extension/customization tools that allow users to customize, connect, and control the components of the application. Examples of extension languages include Emacs Lisp, JavaScript, LotusScript, SIOD (a Scheme dialect, used as an extension language for Gimp, an open source image editing program).

The case for scripting languages as fundamentally different from "system programming languages" has been made prominently in [Ousterhout]. In his conceptual scheme, scripting languages are interpreted, loosely typed, and capable of more abstraction than system programming languages (a category which includes both C and Java). Scripting languages are suited for gluing together components, implementing user interfaces, string manipulation, and rapid prototyping. System programming languages are more appropriate when speed is essential or when programs must implement complex algorithms or data structures.

Contrary to Ousterhout's view, it is possible to use a single language for both high-level scripting and system programming. Examples of systems designed in this way include the MIT Lisp Machine and descendants, which used Lisp at all levels of implementation [Symbolics+++]; and Squeak, a Smalltalk system in which the internal VM is itself written in the high-level language [Ingalls et al 97]. These systems can be enormously powerful development tools, but they tend to create closed worlds, in which everything developed in Lisp or Smalltalk can conveniently interact with everything else; but communication with programs in other languages is awkward at best. Scripting languages, in contrast, are designed to operate in heterogeneous environments where communication between loosely coupled modules is the norm.

Lisp is a protean language which can exist in many forms. It has most recently been been used as the basis for large, closed-world systems and environments; but is now becoming more widely used as a small versatile scripting engine.

Introduction to Lisp and Scheme

Lisp is one of the oldest computer languages still in use. Developed originally by John McCarthy as a formalism for expressing recursive computations and symbol manipulation, it has evolved into a powerful tool for general programming. One unique and powerful feature of Lisp is its ability to conveniently manipulate programs as data. Lisp macros are essentially programs which use this capability to extend the language. Lisp contains two data types, symbols and lists, which are used to represent Lisp programs as well as arbitrary data structures. A list consists of a number of expression surrounded by parenthesis, for example, these are all lists:
(a b c 1 2 3)
((this is) (a list) (of lists))
(define height (* hypotenuse (sin theta)))
()
Lisp performs computations by evaluating expressions to produce values. A Lisp expression is typically a list. The first element of the list indicates a function, while the remaining elements indicate arguments. Numbers are self evaluating; symbols are used as variables but can designate themselves if quoted. Thus the expression
(+ x 1)
means find the value of x, then apply the function + to that and the number 1. Functions are defined by means of lambda expressions, and names are bound to objects by means of define. Thus
(define average
  (lambda (x y)
    (/ (+ x y) 2)))
defines a new function, average, that will return the average of its two numerical arguments:
(average 10 100)  ==>   55
Note that the syntax for invoking a built-in primitive function such as + is the same as that for invoking a user-defined function.

A Lisp listener allows the user to enter expressions from the keyboard and observe the result.

Scheme as a scripting language

Scheme is a dialect of Lisp, developed primarily by Guy L. Steele and Gerald J. Sussman in the mid-70s. Unlike some other Lisps, Scheme was designed to have a rather minimal language core with simple, clean semantics. Its features include lexical scoping, a single namespace for variables and procedures, natural support for higher-order procedures, and space-efficient tail-calling.

Scheme is in widespread use as a scripting and extension language. It has several advantages in this area:

Disadvantages of Scheme

One disadvantage of Scheme is that it is not an object-oriented language, in the strong sense of having user-definable types and an inheritance hierarchy (it is, however, more object-oriented than Java in the sense that all data values are treated uniformly as objects, whereas Java has both object and primitive values). This might mean that it would be difficult to develop an embedding of Java constructs in Scheme. However, Scheme is known for its almost infinite malleability, and several object-oriented extension packages are available. For the purposes of this paper, the lack of a built-in object system in Scheme has not been a handicap.

Since Skij was intended primarily for debugging and experimentation, execution speed was not a primary consideration. In its current state, Skij is implemented as an interpreter and is thus rather slow. Compiling Scheme into Java bytecode is possible (see Kawa), but was not thought necessary for the tasks Skij was designed to serve.

Another disadvantage of Scheme is that it is often seen as a strange or alien language by programmers used to more syntax-heavy languages like C. This problem is partially addressed by a Java-like front-end to Skij (see Jive).

How to embed Scheme in Java

My approach to bringing dynamic interaction to Java was to write a Java implementation of an interactive dynamic language interpreter, and give that language the capability to access Java objects through reflection.

The first stage of this effort involved writing a minimal Scheme interpreter with only a few types and primitives, and no tail-recursion, but with the capability to access Java objects and methods. This produced a proof-of-concept implementation with about a month's worth of work. After that, the implementation was extended to implement all of Scheme, and to provide access to various other aspects of Java functionality such as threads and events.

The basic design principles which Skij uses to integrate Scheme and Java are:

All Scheme values are represented as Java objects. Scheme has a fixed set of built-in object types, including strings, pairs, symbols, and a variety of numeric types. Where possible, existing Java types were made to serve for the built-in Scheme types, even though in some cases this causes small divergences from the Scheme standard. All numbers are represented in wrapped form using the Java.lang.Integer and java.lang.Double classes, and Scheme strings are represented using java.lang.String. Scheme vectors are represented by Java arrays. Skij defines its own classes for pairs, symbols, various types of procedures, and others.

Skij extends Scheme by allowing any Java object (usually including null) to be a Scheme value, used as an element of a list, etc. Internally, Skij typically uses variables whose static type is Object, and casts their values to specific types when necessary.

The Scheme printed representation of a Java object (other than one that corresponds to a Scheme type) consists of the string returned by the Java toString method, quoted by "#< ... >". This ensures that an attempt to read back in a random Java object will generate an error rather than cause unpredictable behavior.

Skij Modes of Use

The Skij interpreter can be run on its own, or it can be run in combination with existing Java classes. There are several different modes of use:

The reflective primitives

The interface between Scheme and Java is centered around a set of seven new primitive Scheme functions. These  access the Invoke package to perform reflective operations on arbitrary objects.

(new class [args]*)

This procedure creates and returns a Java object of the specified class, passing the arguments along to the appropriate constructor. Class can be a symbol, string, or class object.
Example:
(define button (new 'java.Awt.Button "Press Me"))
(invoke object method-name [args]*)
Invoke the named method on the object, with the specified arguments. Returns the value returned by the method, if any.
Examples:
(invoke button 'setText "Please Press Me")
(invoke window 'add button)
(invoke-static class method-name [args]*)
Invoke the named static method of class, which may be a class object or the name of a class, with the specified arguments. Returns the value returned by the method, if any.
Example:
(invoke-static 'java.lang.Thread 'sleep (long 100))
(peek object field-name)
Return the value of the named field.
Example:
(peek (invoke button 'getSize) 'width)
(peek-static class field-name)
Return the value of the named field.
Example:
(peek-static 'java.lang.System 'out)
(poke object field-name new-value)
(poke-static class field-name new-value)
Sets the value of the named field.

Skij implementation details

s

Skij Classes

Like any Java program, Skij consists of a set of classes. There are four kinds of class in Skij: A good deal of Skij is written in Scheme. These Scheme definitions are stored as Java resources and loaded on demand through an autoloading facility. The parts written in Java include definitions for Scheme types and the structures of the interpreter, and about 70 Scheme primitives.
 
Package com.ibm.jikes.skij:
Classes for Scheme types:
Cons
  Nil
EOFObject
InputPort
OutputPort
Procedure (abstract)
  CompoundProcedure
    Macro
  PrimProcedure (abstract)
    ~70 individual PrimProcedures (inner classes)
  Continuation
Symbol

Internal classes:
Environment
  ProcEnvironment
  TopEnvironment
Evaluator
ListenerConsole
ListenerWindow
NullOutputStream
Scheme
SchemeException
  ContinuationException
SchemeListener
SchemeReader (extends java.io.Reader)
SchemeWriter (extends java.io.Writer)
SchemeTokenizer (extends java.io.StreamTokenizer)
Skijlet (extends java.applet.Applet)

Package com.ibm.jikes.skij.util:
Invoke
Tracer
ExtendableClassLoader (extends java.lang.ClassLoader)

Package com.ibm.jikes.skij.misc:
Adaptor
Panel (extends java.awt.Panel)
  DBPanel
GenericCallback (implements AWT listener interfaces)
GenericSwingCallback (implements Swing listener interfaces)
Hashpatch
Kludge
ListenerTextArea (extends java.awt.TextArea)
SkijAppletContext (implements java.applet.AppletContext)
SkijAppletStub (implements java.applet.AppletStub)
SkijSecurityManager (extends java.lang.SecurityManager)

"Package" com.ibm.jikes.skij.lib:
lists.scm
quasi.scm
macro.scm
etc...

The Evaluator

Most of the work of Skij is accomplished in the Evaluator object's eval method. Conceptually, eval takes a Scheme expression and an environment, and returns the result of evaluating the expression in the environment. Some expressions (i.e., numbers) are self-evaluating, that is, the value returned is the expression itself. All other valid expressions are lists. The first element of a list can be a Scheme syntax operator, such as if or define, in which case the evaluator takes the appropriate action. The remaining types of expressions are procedure or macro invocations. In these cases, the first element of the list is evaluated to obtain a procedure or macro object. In the case of a procedure, the remaining elements of the list are evaluated and the procedure's apply method is called with the results as arguments. In the case of a macro, the macro is applied to the original form, yielding a new form which is then evaluated. 

Achieving Proper Tail-calling

The Scheme specification dictates that tail-calling must be implemented in constant space (in other words, stack frames must not be generated for tail-calls). This allows iterative computations to be expressed recursively. A straightforward implementation of the evaluator leads to a violation of this stricture, because Java is not tail-recursive. That is, if eval calls itself, stack frames will always be generated, even if they are unnecessary. The way to avoid this problem is to transform the recursive call into an iteration, which is what Skij does.

Eval is still called recursively when necessary. When a tail-call is done, the evaluator does a reduction rather than a new call to eval. That is, it stays within the current method, replacing the original form with a new one. In the fragment below, which implements the Scheme if operator, the predicate is evaluated with a recursive call to eval, while the then and else clauses are evaluated by reduction:

  static Object eval1(Object exp, Environment env) throws SchemeException {
    while (true) {
        ...
        Cons list = (Cons)exp;
        Object qproc = list.car;
        Cons args = (Cons)list.cdr; 
        if (qproc instanceof Symbol) { // check for special forms
          String sym = ((Symbol)qproc).name; 
        ...
         if (sym == "if") {
            Object predval = eval(args.car, env);
            if (schemeTrue(predval))
             exp = args.cadr();   // reduction, fall through
            else
             exp = args.caddr();  // reduction, fall through
          } 
        ...
        }}}
This technique requires that any part of evaluation that performs in a tail-call must done within the body of the while loop. This forces some violation in modularity. In particular, the implementation of the apply method for CompoundProcedure must be copied so that it can run within the loop.

Scheme procedures

There are four types of Skij procedure objects (all subtypes of the Procedure class). The work of a procedure object is done through its apply method, which accepts two arguments: the execution environment and a list (Cons) of arguments; and returns a value of type Object.

Each primitive procedure is defined as an inner class of the basic PrimProcedure class, with its own apply method. For example, this defines the Scheme set-car! primitive procedure:

new PrimProcedure("set-car!") {
  public Object apply(Environment env, Cons args) throws SchemeException {
    Cons mycons = (Cons)args.car;
    mycons.car = args.cadr();
    return null; }} ;


Compound procedures are simple instances of the CompoundProcedure class, and have their own body, args, and environment attributes. Every evaluation of a Scheme lambda expression produces a CompoundProcedure object. While CompoundProcedures implement apply, more typically they are invoked from the evaluator in the interests of maintaining proper tail-calling. Macros are a subclass of CompoundProcedure and work identically; differing only in the manner of their invocation (see Evaluator).

Continuations have an apply method that throws a ContinuationException. This exception is caught by the invocation of call-with-current-continuation that generated the Continuation object.

Note: Skij does not implement full Scheme continuations. Continuations may only be used as escape procedures; they do not persist indefinitely nor do they allow exited code to be re-entered. This limitation is fairly common in Scheme implementations that use the underlying stack structure for the Scheme stack. Full call/cc requires that control information be heap-allocated or that the stack be copiable.

Environments

Evaluation of a form takes place in the context of an Environment, which is a mapping of symbols to values. There are two types of Environment in Skij: TopEnvironments, which represents the global bindings, and ProcEnvironments, which represent all others (typically a ProcEnvironment is created to serve as the execution environment of a procedure invocation, hence the name). There is only one top-level binding environment, which contains hundreds of bindings, but many ProcEnvironments which typically have 2-10 bindings each. Because of this, they use different implementation strategies.

ProcEnvironments keep track of their bindings in the form of two lists, one holding the names of bound variables, and the other holding the corresponding values. This lets them be created by directly copying the list structures used by the evaluator. Lookup time will be linear in the size of the environment, but since most ProcEnvironments are small this is acceptable.

Although there can be multiple top environments, they all share the same bindings. Rather than keeping them in a linear list, or a hashtable (which has a slow constant lookup time), they use a special form of lookup. Each symbol is given an extra integer-valued index field, which is non-zero and unique for symbols with top-level bindings. When lookup up the binding of a symbol, this value is used to index into a special static array. This produces fast lookup for global bindings (which includes most procedure names). [This idea is due to Peter Norvig].

Vectors

Scheme vectors are represented by Java arrays (generally of type Object[]). This is adequate to support the standard Scheme vector model. However, some Java methods take arguments that are arrays whose elements are a more specific type, including primitives.

To handle these cases, Skij provides a function for generating vectors of types other than Object[]:

(%make-vector n class) [Procedure]

creates a vector of length n whose elements are of type class.
The fact that vectors might be arrays of primitives forces Skij to use the Reflection API (class java.lang.reflect.Array) for vector access, rather than the more obvious method that would apply only to arrays containing reference types. This reflects a decision to simplify the language; a more efficient design might have separate access primitives for arrays of primitive type.

Libraries

A good deal of Skij is written in Scheme and loaded on demand in the form of library files, a process known as autoloading. Library files consist of Scheme forms to be evaluated; they are stored as Java resources along with Skij's class files. Autoloading works by having a table, initialized when Skij is started, that maps symbols with autoloaded definitions to the appropriate library file. If a symbol is referenced and found to be unbound, this table is consulted to see if there is an autoload file. If so, the file is loaded and the symbol lookup is repeated.

Even with autoloading, the process of reading in Scheme files can be slow (or buggy in some Java implementations, especially those found in web browsers). To get around these problems, it is possible to translate the Scheme files into equivalent Java source files which can then be compiled into Java class files. This is not compilation, but merely puts the textual Scheme representation into a format that is faster and more reliable to load.

Any Scheme function can be implemented as a primitive (in Java) or in Scheme. Initially, I tried to minimize the number of functions implemented as primitives, to keep implementation time and size small. However, as Skij evolved, some additional frequently-used functions were made into primitives (and some primitives turned out to be unnecessary, and were rewritten in Scheme). 

How to do Dynamic Dispatch

The Java Reflection API gives a program access to the individual metaobjects representing classes, methods, and fields. While it allows a method to be invoked reflectively, it does not implement any method resolution scheme. In other words, there is no convenient way to simply say: "send object x message y with arguments a*" and have something reasonable happen, which is the level of abstraction needed for dynamic interaction. Thus Skij needed to provide this functionality itself, in the form of higher-level reflective interface.

Normal method invocation in Java relies upon compile-time static type information. Method dispatch is done using a combination of dynamic type information (for the target object) and static type information (for the arguments). In a dynamic interaction environment, static type information is not available. The obvious solution is to substitute the dynamic types of the arguments for the nonexistent static types.

Method invocation in Skij involves the following steps (constructor invocation is similar):

  1. determine the classes of the target and arguments, and assemble the argument classes into an array.
  2. using the classes and method name, see if there is a cached method resolution,

  3. and if so return it.
  4. otherwise, perform method resolution, caching and returning the resulting method.
Method resolution is done according to rules specified in the Java Language Specification [Gosling 96], section 15.11, with some modifications due to the elimination of compile-time processing.
  1. obtain a list of all accessible methods from the target class using the getMethods() call.
  2. examine each method to see if it is appropriate for this call:
  3. if there are >1 qualifying methods, the most specific is selected using rules from JLS 15.11.2.2
Invoke-assignablity is an extension of the usual notion of Java type assignability (see JLS 5.2), which does not permit assignment between primitive and reference types. If the parameter type is a reference type, then invoke-assignability reduces to regular assignability. If the parameter type is a primitive, the argument type is invoke-assignable to it if the unwrapped type of the argument can be converted to the parameter type through a widening conversion (see JLS 5.1.2).

In Java 2, this procedure is slightly different, since it is possible to invoke non-public methods. Instead of using the getMethods call, Invoke uses getDeclaredMethods the target class and on all of its ancestors. When a method resolution is found, it is set to be accessible using the new AccessibleObject interface.

Access to Other Java Facilities

In addition to Java object manipulation facilities, Skij includes features that provide access to other elements of the Java environment such as threads, events, synchronization, and exception handling. Because Scheme is capable of representing procedural objects, these features were implementable without alterations or extensions to the basic Scheme language (that is, no new syntax or special forms are required). Instead, they rely on combinations of new primitives, macros, and specialized Java classes.

Exception Handling

Skij provides the ability to wrap Scheme code in a form that will cause exceptions to be trapped and returned as objects (performing the functions of try/catch in Java).

(catch . body) [Macro]

Evaluate body. If an exception occurs during the evaluation, the exception is trapped and the exception is returned as the value of the catch form. Otherwise the value returned by the last body form is returned.
(%catch thunk) [Primitive]
The primitive on which catch depends. A thunk is a procedure of no arguments. (catch body) is expanded to:
(%catch (lambda () body))


(throw exception) [Primitive]

Causes exception to be thrown.


(error string)

Create and throw an exception containing the message string.

Synchronization

Access to synchronization is implemented in a very similar manner to catch.

(synchronize object . body) [Macro]
(%synchronize object thunk) [Primitive]

These functions arrange for body or thunk to be called within a context in which object is locked.

Threads

Threads are handled by making CompoundProcedures implement the java.lang.Runnable interface. This means that any thunk can be used as the base object for a new thread.

(in-own-thread . body) [Macro]
(run-in-thread thunk) [Procedure]
    Run body or thunk in its own thread. run-in-thread is defined as follows:

(define (run-in-thread thunk)
  (define thread (new 'java.lang.Thread thunk))
  (invoke thread 'start)
  thread)

Event Handling

 Java's user interface toolkit (AWT) provides an object-based model of event handling. Events themselves are objects, and application objects can choose to implement one or many of the standard AWT Listener classes (i.e., java.awt.event.ActionListener). A listener registers to receive events from an object by calling that object with an add...Listener method:
   (new java.awt.Button()).addMouseListener(listener);
We'd like to be able to specify the response to a mouse event in Skij. To accomplish this, Skij includes a Java class called GenericCallback that implements all of the AWT Listener interfaces. A GenericCallback object contains a Skij procedure of one argument. When the callback object receives an event, the procedure is applied to the event object.

Thus the following Skij commands will create a button that prints a message when pressed:

  (define b (new 'java.awt.Button "Press Me"))
  (define listener (new 'com.ibm.jikes.skij.misc.GenericCallback 
                        (lambda (evt)
                           (print "Thanks, that felt good"))))
  (invoke b 'addActionListener listener)
It would be possible to define a separate callback class for each type of event listener, but it was not necessary since events are self-describing. 

Other Interfaces for Dynamic Interaction

The ability to dynamically interact with the Java environment is valuable, but Skij as an interface to this ability presents some problems. Not all Java programmers are conversant with Scheme or willing to learn; and as a text-based interface it has inherent limitations that can be overcome with GUIs and other techniques. This section describes some other Skij-based techniques for interacting with Java objects, either alongside or instead of Skij itself.

Object Inspector

The object inspector provides a simple tabular view of a single Java object. Each row of the inspector displays an object member (field or get-method) along with the corresponding value (note: the values are in Scheme notation; hence the quoting with "#< #>" for most Java objects).

Most objects in the java package have few or no publicly accessible fields, and instead present their state to external users through bean-style get methods. Thus, the JDK 1.1 version of the inspector shows the values of all get methods (defined to be zero-argument methods that begin with get or is). In Java 2, non-public fields of objects can be accessed and can be displayed directly.
Tabular Inspector View
A typical use of the inspector is to traverse the object structure starting from a known object until the object of interest is found. Often one wishes to perform operations on this object. Skij includes a variable, inspected, that is always bound to the object contained in the topmost inspect window. This lets the user refer to an object obtained via the inspector from a Skij listener.

In some cases the slot/method view of an object is not very useful. For example, inspecting a list in this fashion would only show the car and cdr of the first pair. So, for certain object types, the inspector shows the contents in a different form. The object types treated specially include lists, arrays, Vectors, Enumerations, and Hashtables.

The inspector gives a view of selected Java objects, but all fields of interest in Java are associated with objects. Static fields and methods are associated with classes rather than individual instances. In order to make these accessible via the inspector, they are displayed as additional fields when inspecting a Class object.

Graph Inspector

The tabular inspector described above is designed to show all known information about a single object. Viewing systems of relations between objects requires a different kind of graphic representation. Skij provides a graph inspector which can illustrate such relationships.

Graph Inspector View

The graph inspector represents objects as rectangles. Color is used to indicate object type. Relations are indicated by labeled lines. Objects and relationships are created on demand, by the user, using a pop-up menu. The graph inspector can be linked to the tabular inspector, so that browsing through one will also bring up objects in the other.

The graph inspector is also useful for revealing hidden facts about object identity. Any given object is drawn only once in the object graph, so the fact that two access paths lead to the same object is readily visible. It's also easy to spot unexpected multiple objects (for instance, I was surprised to learn that the getGraphics() method on AWT components always returns a unique new Graphics object every time it is called, in at least one Java implementation. This fact would be difficult to notice in a textual interface, sine the objects print identically). 

WebSpect

WebSpect is an attempt to provide dynamic interaction through a web-based interface. Using WebSpect, a user can inspect and manipulate a Java VM running on a remote machine using a web browser. Although implemented in Skij, Scheme is not visible to the user.

Inspection

The inspection facility of WebSpect reuses a large part of the code for the tabular inspector, but generates a dynamic HTML page rather than a more conventional GUI interface. When values are displayed, they are annotated with hyperlinks that allow traversal of the object structure. The nature of web-based interfaces makes it easy to add annotations and special-purpose displays to pages. For instance, when inspecting a class, WebSpect will show its position in the class hierarchy as well as its slots; when displaying an array, WebSpect will annotate it with the size. It is also easy and natural to include links to documentation.

Manipulation

Inspection is fairly natural in a HTML-based interface, but manipulation operations are less so. The basic operations on objects (other than those handled by inspection) are creation, modification, and method invocation. These operations can require arguments, which is a problem for a web-based interface. How are the arguments to be specified?

WebSpect presents interfaces to operations by means of interface templates that correspond to allowed constructor or method calls. When one of these operations requires an argument, it provides an appropriate interface widget. Strings and numbers can be typed directly into text boxes; booleans are entered by means of a check box. Other types of object are more problematic. How do you specify, say, a Locale? The solution is suggested by the fact that the server already tracks known objects, so for any argument, it can find all the objects of that type that it knows about, and offer them in a pop-up menu.

WebSpect arguments also contain a link to the class of the argument, which allows a user to go off and create an object of the appropriate type, come back, and use it as an argument.


 

Implementation

WebSpect is essentially a simple HTTP server written entirely in Skij. The browser supplies commands and arguments to the server via HTTP, and the server responds with a dynamically generated web page. The server maintains an index of all known objects, which allows the browser to refer to them. For instance the URL:
  http://fury.watson.ibm.com:2341/webspect/inspect?object=19
indicates a request to inspect the object with index 19. A more complex request can include multiple parameters:
  http://fury.watson.ibm.com:2341/webspect/create?class=14&po0=19&po1=22
indicates a request to create a new object from the class whose index is 14 and with two parameters with indices 19 and 22.

The server translates the incoming URLs to calls to special page-generating procedures, i.e.:

(define-command create (class)
  (set! class (code-object class))  ; turn the index into a class object
  (define arguments (decode-parameters params))  ; translate the p0n style arguments
  (define object (apply new class arguments))
  ... generate a page describing the new object ...
)

Jive: A Java-like interpreted language

Jive is a Java-syntax front-end to Skij. Jive implements a language that is basically a Java subset, without type declarations. The subset includes object creation, field access, method invocation, and array operations, but omits class and method definition and all control constructs. Jive is designed solely for dynamic interaction rather than programming, although it would not be difficult to add support for control constructs.

Jive is built from a Scheme-based parser generator (written by Mark Johnson) and a LALR(1) grammar that translates Jive expressions into Skij forms. The Jive listener reads Jive expressions, translates them using the grammar, and evaluates them.

Related Work

Other Schemes in Java

There are other Scheme implementations for Java, However, none of these offer dynamic Java method invocation, and so are less useful as Java scripting tools. New Scheme implementations appear on a regular basis; in part because Scheme is so easy to implement. For an up-to-date list see this web page

Kawa

Of the other known Scheme-in-Java implementations, Kawa is probably the most fully developed. It includes a compiler that translates Scheme into Java bytecodes. While this results in much faster execution speed, it can also violate Scheme's tail-calling rules. Kawa includes facilities for invoking arbitrary Java methods from Scheme, but it does not perform method lookup at runtime and requires exact specification of the method signature, so it is not well suited to interactive use.

SILK

SILK (Scheme in 50K) is close in spirit to Skij, being an interpreter designed primarily for simplicity. It lacks most of Skij's facilities for manipulating Java objects and facilities.

One way in which SILK differs from Skij is that it uses the Java null value to represent the Scheme empty list. Many loops in Scheme code or internal loops of the interpreter are terminated on the empty list, and testing for null is generally fast in Java, so this is a speed advantage. Any Java variable of reference type can contain the null value, but null is otherwise atypical. A null "object" cannot be the target of a method invocation and cannot be stored in a hashtable. Thus using the Java null value for Scheme's empty list can complicate the interpreter and give rise to irregularities.

Other Java tools for dynamic interaction and scripting

VisualAge

IBM's VisualAge for Java environment contains a Java interpreter which allows a degree of dynamic interaction, as well as method redefinition. However, it relies on a special JVM and cannot run outside of the development environment.

BeanShell

BeanShell is a Java-syntax scripting and interaction environment that runs in the standard JVM. It implements most constructs of the Java language, and (as far as I know) it is the only embedded interpreter besides Skij to do run-time method lookup properly.

Current and Future Status

Skij has been publicly available with a limited license on IBM's alphaWorks site since mid-1998, and is in active use by an estimated several hundred people. Applications include data base manipulation, web servers, component glue, education, and debugging.

Future Extensions

Integrate various interfaces to form an environment

Currently, Skij and the related interfaces for dynamic interaction for a set of loosely-interconnected tools. It would be possible to encapsulate them in an IDE-like framework that would support standard Java development along with dynamic interaction.

Compilation

Compilation of Skij to Java byte code would bring two important benefits to Skij: speed improvements, and the possibility of defining Java classes and methods in Scheme. 

Full call-with-current-continuation

Call-with-current-continuation (call/cc) is a somewhat exotic feature of Scheme that allows a program to capture the full control state of the interpreter. It has applications in advanced control structures (i.e., intelligent backtracking) and in distributed computation. Full implementation of call/cc requires stack manipulations that aren't supported by the Java VM. If full call/cc is to be supported, it requires either VM modifications, or forgoing the use of the Java stack in favor of a heap-allocated invocation frames. The latter would be easy enough to implement in Skij, at some performance cost.

More dynamism in Java

Skij does just about everything it can to achieve dynamic interaction within the confines of the standard JVM. There are other features of dynamic environments, such as method redefinition and flexible error recovery, that are difficult or impossible to implement in the standard JVM. Custom or specialized JVMs, such as those found in some commercial development environments, would enable these features to be explored.

References:

Ousterhout, John K. Scripting: Higher Level programming for the 21st Century, IEEE Computer, March 1998, http://www.scriptics.com/people/john.ousterhout/scripting.html

Travers, M, Programming with Agents: New Metaphors for Thinking about Computation, Ph.D. dissertation, MIT Media Lab 1996.

diSessa, A. A. and H. Abelson (1986). "Boxer: A Reconstructible Computational Medium." CACM 29(9): 859-868.

The Java Language Specification, Gosling, Joy, Steele, 1996.

Scheme standard, Clinger et al.

Ungar, D. and R. B. Smith (1987). Self: The Power of Simplicity. OOPSLA '87, Orlando, Florida, ACM Press.

Back to the Future: The Story of Squeak, a Practical Smalltalk Written in Itself, Dan Ingalls, Ted Kaehler, John Maloney, Scott Wallace, Alan Kay, OOPSLA 97.