March 93 - Camelot — Not Just Another Pretty Face
Camelot — Not Just Another Pretty Face
Juan Guillen
Camelot is an object oriented environment which has been built "from the ground up
by developers for developers. Camelot is designed from first principles rather than
being based on other products, and it does more than making it easier to write
graphical user interfaces.
The environment incorporates some significant new capabilities for the management of
the complexity introduced by large system developments. One of these is an additional
level of modularity which permits individual components to be created independently,
then combined with other components to create a complete system (see Figure 1). The
use of object repositories to store both objects and instance specific methods provides
an additional degree of encapsulation, and its open architecture guarantees full
interoperability with existing systems.
Camelot is supported by a comprehensive visual development environment.
Nevertheless, as some of the concepts which we are going to discuss are best expressed
syntactially, we begin with a background description of Camelot's Fire language.
Fire
The Fire language is a strongly typed, late binding language which has been designed to
express fundamentally procedural logic in an object oriented context. Its data types can
be characterized as follows:
• Object, which references a Fire object.
• Standard Types, which include integer, float and string.
• Utility types, including pointer, which is used to store information useful
to foreign systems, and variable, which assumes the data type of the value it
contains.
• Structured types other than object, including record, field and collection.
A variable of type collection can contain any number of objects of any class,
and can be sequential or keyed.
Variables are accessed either normally (one := two), hierarchically (one +=
two.three.four) or indirectly (one.two -= <<"th"+"ree">>.four.<<"fi"+"ve">>). In the
indirect form, any expression which results in a string value can be placed between
the <<>> bracketing. Fire implements an hierarchical variable name resolution
scheme which searches for a variable first in the method, then in the object, etc. until
it searches the global name space. These levels are method -> object -> connected
object -> module -> global, and are discussed in greater detail below.
The code fragment below illustrates a several Fire principles.
01 #include "DCCDefinitions.D
02 #ifndef DCC
03 # define DCC 5
04 #endif
05 method open( arg integer ) returning string
07 name string : initial value "DNX";
08 element object.dcc(ne);
09 if ( arg:assigned )
11 name := elements[ DCC, arg ].name;
12 }
13 if ( name:length >= 17 )
15 [ New(element) ConnectionAddFirstTo( self ) ];
16 }
17 return ( name );
18 }
Lines 01 to 04 show that Fire incorporates a full preprocessor which allows the usual
#include, #define, #if(n)def etc. directives.
Line 05 is the standard Fire method definition line, in this case for a method called
open with an argument called arg of type integer. The method returns a string value.
All Fire methods run in the context of an object of a minimum class, i.e. the class in
which the method is defined (the "method class") or one which inherits from the
method class.
Lines 07, 09 and 13 show that Fire variables have attributes -in this case, an initial
value, an assignment status and a string length. Assignment status (:assigned) can be
both read and set, and is particularly useful because it prevents the use of a variable
before it has been assigned.
Line 08 shows that Fire variables of type object can have a minimum class as defined
above which further qualifies their data type. In this case, the minimum class of the
element variable is dcc in module ne.
Line 11 illustrates access to a two dimensional array called elements. Any Fire
variable can be defined to be an array of any number of dimensions.
Fire has standard procedural flow of control constructs, as illustrated in the code
fragment below: if-else, while, for each, for ( ), break, continue and return.
if ( boolean )
{
while ( x < y ) {
for each element in collection {
for ( index := 0; : index < 10 : index += 1; ) {
if ( condition( index ) ) {
break;
}
else {
if ( !otherCondition ) {
continue;
} } } } }
return ( x >= y );
}
Fire also has an expanded form of the case statement and a full set of both logical and
arithmetic binary and unary operators, as well as the tertiary ?: construct found in C.
case ( complexCalculation() )
{
= valueOne: {
x *= ( y + 3 ) % 4;
}
!= valueTwo: {
x = ( y & z ) | w;
}
~^ valueThree: {
x /= ( w < 1 ) ? -w : w;
}
> valueFour| x > y: {
}
default:
{ }
}
Methods in Fire are always executed in the context of an object, which is called the
method object. "Sending a message to an object" is synonymous with "executing a
method in the context of an object." Some examples of method invocation are shown
below:
01 close( x.y, 142 + 857, subvalue() + 3.89 );
02 close() : super;
03 [ otherObject close() ];
04 [ findObject( name ) close() : if defined ];
05 [ otherObject update(), display(), close() ];
Line 01 invokes the close method in the current object context, passing arguments
x.y, 142 + 857, etc. Line 02 also invokes a close method in the current context, but
the method it invokes is at a higher level of inheritance-the inherited close method.
Line 03 invokes the close method in the context of otherObject. In fact, it invokes the
close method defined by otherObject or by its progenitors. Line 04 shows that the
object context can itself be any expression which returns an object reference, and the
if defined clause means that, if the close method is not defined, processing continues
without error.
The construct shown in line 05 is a convenience; the update, display and close methods
are all invoked in the context of otherObject. All return values except that of the last
method invoked, in this case close, are discarded.
In addition to these syntactic constructs, the entire Camelot environment has been
designed to support the object oriented paradigm, especially the principle of
encapsulation.
Asynchronous Methods
Any Fire method, regardless of the language in which it is written, may be invoked
asynchronously. This is illustrated in the code segment below, which invokes the
update method in the context of the object referenced by the window method. update is
invoked asynchronously, to start after delay seconds and to be executed repeatedly
every repeat ticks.
method
startUpdate( delay integer, repeat integer, arg float ) {
[ window()
update( arg ) : asynchronous
, after delay seconds 0 ticks
, every 0 seconds repeat ticks ]
}
Connections
Fire objects can be dynamically connected to other objects at run time, and can have
many objects connected to them in turn (see Figure 2). This mechanism is useful for
two reasons: first, the identities of connected objects in either direction can be
determined and connected objects can be sequenced; and second, the connected objects
are used for variable name space resolution. This means that, in the example below,
the class C object at the lower right has access to variables W, X, Y, Z, B and Ø
directly, and any method executing in the context of this object would be able to refer
to all of these variables, including W and X, as if they were defined in the object itself.
Instance Specific Methods
One of the difficulties encountered by other object oriented environments is the
proliferation of classes. This happens for two reasons: the need to have all classes
inherit from a common progenitor and the need to create a new class ("subclass")
whenever even a small degree of specialization is required for a particular object.
Fire's solution to the former is the introduction of modules, which are discussed in
other sections below. The need to subclass for single instance specializations is
addressed by instance specific methods, which allow specialization without the
associated administrative overhead.
To understand instance specific methods we must start with an explanation of Fire's
normal method dispatch mechanism (see Figure 3). Although this mechanism is
optimized, it operates completely in accordance with the theory illustrated in the
diagram below. All Fire objects reference a dispatch table which contains a list of
methods in the order in which they were defined. The diagram illustrates an object of
class C which inherits from class B which inherits from class A. The object in question
has six methods defined: from the bottom up, Z, Y, super Y, X, super X and W. Let us
assume that the object in this example receives a message Y. Its dispatch table is
searched from the bottom up and the method defined by class C is executed.
Figure 4 shows instance specific methods Y, Z and Ø which have been added to the
object in question. References to these methods are appended at the bottom of the
dispatch table for the object, and when it receives a message Y it invokes the instance
specific method, which is the first one found when searching the table from the bottom
up. The first super invocation resolves to the method Y which was defined by class C,
and the next to the method Y which was defined by class B.
Instance specific methods are a convenient way of implementing specific functionality
which is only required by a single instance of an object. A good example of their use is
in specifying the action of a button object on a panel, which in all probability is an
action unique to that object. Of course, if the object is duplicated (e.g., to create a new
copy of the window) so is the reference to its instance specific methods.
Object Repositories
The usefulness of instance specific methods is greatly increased when they are used in
conjunction with Camelot's object repositories. When a Camelot object is placed in an
object repository, all of the objects which it explicitly references are stored with it.
This includes objects which it references directly or through collections, and those
which reference the object through Fire's connection mechanism. Since Camelot's
object repositories are implemented in host operating system files, they can be as
large or as small as necessary, making it possible, for example, to place all of the
objects which make up a window into a repository.
Camelot's object repositories are stored in both binary and machine independent
forms, which means that any repository can be copied to any machine which Camelot
supports. Camelot recognizes that the repository comes from a different machine and
recreates its binary form, providing immediate portability.
This combination of object repositories and instance specific methods makes it
possible to introduce new functionality without increasing the size of the environment
as a whole. It also extends the benefits of encapsulation to a higher level by allowing
the developer to ignore encapsulated functionality which does not pertain to the
problem at hand. In addition, it makes distribution of new functionality a
straightforward matter of copying a file from one system to another.
Modules
Object oriented development efforts are often characterized by rapid progress in the
early stages, followed by increasing difficulties and, in the end, the inability to
implement required functionality. This pattern is due primarily to the inheritance
mechanism, which in any substantial development results in a multilayered tree
which is difficult to understand and even more difficult to modify.
To circumvent this difficulty Camelot implements modules, which are higher level
encapsulations of functionality. Camelot module definitions organize separate classes
and methods into a self-contained unit with a well defined interface and a single
objective, in much the same way that classes themselves organize data definitions and
their associated methods. This means that a portion of a system can be developed and
modified with a greater degree of independence.
External Code
Camelot provides a simple mechanism for declaring that functions stored in DLLs or
code resources are to be treated as methods for a specified class. Although this is not
strictly speaking an encapsulation feature, it does make external code appear to be
defined specifically for a Camelot class. The object oriented model is maintained as far
as possible in that the resultant methods are defined for their associated class and all
subclasses, and can be overridden by subclasses as necessary. In addition, functions
from one DLL can be associated with several different classes, and a single class can
have functions from several different DLLs.
Garbage Collection
Camelot's developers maintain that encapsulation is impossible without garbage
collection. In a system that is not garbage collected and in which object A refers to
object B, it is necessary for object A to know when it can tell object B to destroy itself.
This functionality can not be implemented by object B, even with reference counts,
which suffer from isolated mutual references or "disconnected loops." It is therefore
necessary for the developer writing object A to have knowledge of the system's overall
architecture, which limits the size and complexity of systems which can be created to
those which can be understood by a team of programmers.
Example
One way of describing such a graphical environment is used is to provide a practical
example of its use. Our task will be to implement a simple panel which will convert
between miles and kilometers (this example is shown and explained in Figures 5-12).
We should note here that Camelot's philosophy is to use native mode controls (buttons,
check boxes, etc.) whenever possible, although the user always has the option of using
a control defined by Camelot. This means that, at the user's discretion, all buttons can
look the same-or that a button can look like a Macintosh button on Macintosh
computers, a Windows™ button under Windows, etc. This is an option settable both as a
user preference and on a control by control basis.