Conformance of agents in the Eiffel language
Philippe Ribet,
Cyril Adrian, Olivier Zendra, and Dominique Colnet
INRIA - CNRS - University Henri Poincaré, Vandoeuvre-lès-Nancy Cedex
|
 |
TOOLS USA 2003
PROCEEDINGS

PDF Version |
Abstract
In Eiffel, the notion of agent makes it possible to describe and manipulate
computation parts (i.e. operations) like ordinary data. Operations may
be partially described, may
be passed as ordinary data and may have their execution delayed. Agents are very
convenient for many purposes, such as going through data structures and implementing
call-backs in graphical libraries.
Although they can be seen as normal objects, they convey specific issues, pertaining
to standard conformance rules for generic types. To get rid of existing problems,
this paper proposes an adaptation of conformance rules for agents that provides much
more flexibility while retaining all the benefits of a strong static typing system.
1 INTRODUCTION
Agents were introduced in the context of the Eiffel language
in 1999, as an extension [DHM+99] for the previous definition of Eiffel [Mey92]. Although a
number of details were provided for their typing, we realized when implementing
agents in SmartEiffel1, The GNU Eiffel Compiler
(http://SmartEiffel.loria.fr)
in Summer 2001 that major issues remained. Being among the first to implement
agents in an Eiffel compiler and to actually use them (for iterators, for
a graphical library still under work, etc.) enabled us to gather significant experience
in this area, and made us find solutions to the uncovered issues.
This paper aims at presenting
those solutions. It is organized as follows. First,
section 2 presents the concept of agents in Eiffel. Section 3 then
explains the severe
issues that arise when using the usual conformance rules with agents.
The solution
we suggest is detailed in section 4; its goal is to tackle these
issues and allow agents
to smoothly merge with all other Eiffel concepts. Finally, section
5 concludes.
2 AGENTS: PRESENTATION
Overview
The agent mechanism [DHM+99, Mey00] gives the Eiffel object-oriented
language
the ability to handle operations, or commands, as such, like in functional
programming
languages.
Agents are a new type of objects that allow to store code
to be executed and
data in an object, named the agent. In Eiffel, four types of agents
exist: an abstract
(deferred) type ROUTINE, and three concrete types PROCEDURE, FUNCTION
and PREDICATE. The following figure shows their inheritance
relationship:

Agents are a way
of storing operations for later execution. They are objects.
As such, they can be stored, compared to Void, or passed around
to other software
components. The operation stored in the agent may then be executed
whenever
the component decides. The most common uses of agents comprise
delayed calls,
multiple calls (on different values), lazy evaluation, and so on.
Using agents in Eiffel
Standard method calls are executed exactly “where
they are written in the code”. An
agent, while having a syntax similar to a simple feature call, does
not immediately
call the method. Instead, an object is created, to be stored and used
later. Only
when it is used, will the agent trigger the feature call.
The created object has, like any Eiffel object, a well defined static
type. The
agent type (be it ROUTINE or one of its heirs as shown above)
may be used to
declare entities, such as attributes, feature parameters or a feature
result.
For illustration, we use in this paper examples inspired from graphical
user interface
(GUI) programming. As the following example shows, GUI usage widely
benefits from the power of the agent mechanism:

The expression starting with the agent
keyword on line (1) creates
a new object,
actually an agent object, instead of executing the print_coordinates
method
immediately. Note the pair of question marks (?,?) which
denote the fact that
the arguments of print_coordinates are not yet given. Still at line
(1) this newly
created agent object is passed to the when_pointer_move method
to be memorized
by the my_window object of class WINDOW. Thus the operation saved by
the my_window object may be executed many times,
with different arguments (e.g. each
time the mouse pointer moves inside the WINDOW).
The WINDOW class
is in charge of the agent memorization as well as the agent
execution when the move event occurs. The following extract of the WINDOW class
shows how to declare an attribute to store the agent (2) and
then the usage of the call feature (3) to
launch the execution:

The method when_pointer_move saves
the agent, while the method pointer_move_dispatch
executes it using the mouse pointer coordinates as arguments.
It is
interesting to focus on the PROCEDURE[ANY,
TUPLE[INTEGER, INTEGER]] type.
It is the type of an agent that stores a procedure. This procedure
may belong
to (be defined in) any class. Such an agent has two open arguments
of type INTEGER.
An open argument is an argument whose value is unknown when the agent
is
created, but is provided at call time (when the agent is executed).
Conversely, an
agent may also have closed arguments, that is arguments known when
the agent is
created. Hence, the agent object does not only refer to a routine
(as a mere function
pointer would in other languages), but also contains additional information
that
becomes available as arguments of the executed routine. For example,
the previous
code can be changed this way:

Open arguments are symbolized by question marks, while closed
arguments are
directly stored in the agent object. This powerful system makes it
possible to have
delayed calls with values specific to each call (open arguments) and
values specific
to each agent but common to all executions of this agent (closed arguments).
This
mechanism is secure because each argument type is checked at compile
time.
In the above example, the pointer_move_dispatch procedure
will thus use the
same call on all agents, even though some executed procedures require
two formal
arguments and others need three.
Another capability that makes agents
very useful is that any existing method can
be turned into an agent, without any change to its code. We could use
for example io.put_string:

Common agent use cases
The first goal of agents is to delay
calls. Here is one example of such a use: let’s
imagine you have a dog, which is able to do what you tell it do do
at noon. You
may tell it to eat, to walk, to sleep, to get the newspaper...
Below
is the code preparing the action the dog will do at lunch time (we
ask the
dog to eat some_food then). Follows an
extract of the DOG class showing
what it
does with such an instruction (see the do_lunch action).

Another common use of agents is repetitive action on a collection.
For example, ARRAY features the do_all method whose code is:
[code 6]
The following example shows the basic use of do_all:

In the above example, we display the name of each animal
in the zoo. Indeed
when zoo, an ARRAY[ANIMAL], is asked to
do_all on line (5), it calls
the print_name agent once for each item it contains
(line (4)). Thus, each
ANIMAL in zoo is passed as an argument to
print_name,
which then prints its name (line (6)).
Agents
also allow the receiver of the call (the target) to be an open argument.
In this case, the open target is denoted by its type, like in the following
adaptation
of the previous example:

Note that in this case, do_all is still used.
Line (4) executes
do_lunch using
all the item(i) as successive targets,
hence triggering do_lunch on every ANIMAL.
Agents
are a way to pass code as an argument, but they may also be used for
partial execution. In method calls, all parameters are evaluated before
the call. If an
actual parameter is agent object.method(arg),
then it results in an agent object
creation and method is not called on target object.
Note that arg is
evaluated when
it is stored in the agent object. This agent may be executed later
when requested.
The following example shows partial code evaluation on an academic
example:

All examples shown so far use PROCEDUREs; using agents of type FUNCTION
is very similar. An agent of type FUNCTION requires
one more generic parameter
for the function result type. For example, with the following function
definition in
class FOO:

Various agent types may be used, as the following table shows:

Note that the PREDICATE type is just a shortcut for a FUNCTION with
a BOOLEAN result type: PREDICATE[A, B] is equivalent
to FUNCTION[A, B, BOOLEAN].
3 STANDARD EIFFEL CONFORMANCE RULES
Conformance usage
The Eiffel conformance rules are involved in assignments,
be they direct or indirect.
For example, let’s consider the following code:

With these declarations, a direct assignment a
:= b is valid only
if type B conforms
to type A, while an indirect assignment foo(a,
b) is valid only if
type A
conforms to type C and type B conforms to type D.
Note that this is an indirect
assignment because the call foo(a, b) assigns
effective parameters to formal parameters: in order to initialize the
formal parameters, c
:= a; d := b is performed
when entering the foo routine.
The conformance
rule in assignments is the base of the typing system. The goal
is to be sure that an object has a dynamic type which conforms
to the static type of
the entity used to access the object.
The assignment attempt construct
offers the possibility to write an assignment
that would be invalid according to the previous rule based on static
types. a ?= b is an assignment attempt. Such an instruction succeeds only if the
dynamic type
of the source b of the assignment conforms to the static type of the
target a. The
conformance rule is thus satisfied.
The next parts define the precise
conformance rules in Eiffel.
Conformance with basic types
Very briefly, the main conformance rules
are:
- any type conforms to itself,
- an expanded type conforms to the relative reference type,
- if types A and B are not expanded, and class B directly
inherits from class A, then type B conforms to type A,
-
the ‘conforms to’ property is transitive.
More details can be found in the Eiffel reference manual [Mey92].
Conformance with generic types
Conformance with TUPLE types

Conformance with ROUTINE types
The ROUTINE types are generic types with more semantic. As ROUTINE
types do not have their own conformance rules one may think that the
generic types rules apply. We will hold true this assumption in this
chapter, and show that we can make dogs eat tomatoes.
The ROUTINE type is a generic type with two
formal type parameters: ROUTINE[BASE, OPEN –> TUPLE].
According to the conformance rule for generic
types, a type ROUTINE[B1, O1]
conforms to a type ROUTINE[B2, O2]
only if B1 conforms to B2 and if O1
conforms to O2.
As mentioned in section 2,
page 2, the PROCEDURE type inherits from ROUTINE,
and has the same formal type parameters: PROCEDURE[BASE, OPEN –>
TUPLE].
Its conformance rule is thus the same as for ROUTINE.
The FUNCTION type
is a generic type with three formal type parameters:
FUNCTION[PROCEDURE, OPEN–> TUPLE, RESULT_TYPE].
According to the conformance
rule for generic types, a type FUNCTION[B1, O1,
R1] conforms to
a type FUNCTION[B2, O2,
R2] only if B1 conforms to B2,
O1 conforms to
O2 and if R1 conforms to R2.
PREDICATE[B, O] type being just a shortcut
for FUNCTION[B, O, BOOLEAN],
and it has the same conformance rules as ROUTINE.
The next
section shows these conformances within the context of various examples.
Applying
conformance rules to examples: issues arise
Having precisely recalled
the rules of conformance for the different types, we now
apply these rules to an number of examples, in order to show that
issues arise.
Let’s start with the following code:

If we consider line (8), the type of lunch_action
is PROCEDURE[ANY, TUPLE[FOOD]] and the type of agent my_dog.eat(?)
is PROCEDURE[DOG, TUPLE[MEAT]]. As DOG conforms to ANY
and TUPLE[MEAT] conforms to TUPLE[FOOD] (because MEAT conforms to FOOD,
being a subtype of it), the conformance
rules of generic types implies that PROCEDURE[DOG, TUPLE[MEAT]]
conforms to PROCEDURE[ANY, TUPLE[FOOD]].
Thus, according to the standard
conformance rules, the assignment on line (8) is
a valid one.
Using the same rules, line (9) is
valid. The definition of call is call(o:
OPEN); in this example, OPEN corresponds to TUPLE[FOOD], because
of line (7). [tomatoes] is
thus a valid argument.
As a consequence, the call on line (9) executes
the eat method
of class DOG (line (10)) with tomatoes as
effective argument which does not conform to the
formal argument type MEAT. This odd situation results in a very
dangerous state,
because the dynamic type of meat does
not conform to its static type; this violates
the conformance rule stated earlier. It seems reasonable to consider
this a major
problem.
Let us now formally demonstrate that the rule leads to conformance
oddities.
Our next example considers an agent as a delayed call. If an agent
is executed where
it is created (without any instruction between the agent creation
instruction and the
agent execution), then the agent call and its execution should
have the same effect
as a direct call and the properties should be similar.
Let’s consider
the following class definition:
[code 15]
With the previous definition in mind, we now examine several calling
sequences
variants that should be equivalent:

Writing the (12) assignment is appealing. Indeed, the type of
gb, found
on line (11), guarantees the method will be called with a parameter
conforming to B (on line (13)). And the f method
is precisely one that accepts such parameters
(since it accepts A, to which B conforms). Thus, (13) should
be valid and work
as expected. However, the assignment on line (12) is
forbidden by the current
conformance rules in the language, because PROCEDURE[T, TUPLE[A]]
does not
conform to PROCEDURE[T, TUPLE[B]], since A does not conform to
B.
This example makes it clear that the typing system may prevent
writing perfectly
valid calls, which is not satisfactory.
Let’s now consider the
following normally equivalent calling sequences:

Line (14) is invalid, since method g requires
a parameter of type B, not an A.
Conversely, line (15) is valid, because agent g is
of type PROCEDURE[T, TUPLE[B]],
which conforms to PROCEDURE[T, TUPLE[A]],
the type of
fa. Line (16) is
also valid, since the provided parameter type is the one expected,
A. But
the result of running lines (15) and (16)
is to execute the g method
with a as
parameter, which would be normally invalid in a direct call and
may not reasonably
be expected to succeed.
Thus, we can conclude that the normal conformance
rules applied to routines
makes it possible to defeat the typing system and perform invalid
calls, which is an
issue.
In the previous examples, we studied cases for the conformance
of the parameters
of the TUPLE type. But conformance with TUPLE types encompasses
another
aspect: the number of parameters, or size of the tuple. The next
examples pertain
to this second aspect.
The code of the following example uses the one
for the WINDOW class provided
on page 3:

On line (17), the type of
agent print_all2 is
PROCEDURE[T, TUPLE[INTEGER,
INTEGER, TIME]]. Since the argument type for when_pointer_move
is PROCEDURE[ANY, TUPLE[INTEGER, INTEGER]]
(see line (2) page
3), the indirect assignment on line (17) is allowed according
to the previous conformance rules.
However problems are bound to arise
when the agent is triggered
and the call is
executed. Indeed, the call on line (3) page
3 corresponds to action.call([x,
y]) but the actually executed method print_all requires
one more argument (line (20)).
That is a dramatic error that is undetected by the type system,
which is likely to
cause trouble because this method needs information it will never
get.
On line (18), the type of agent
print_x is
PROCEDURE[T, TUPLE[INTEGER]].
However, the argument type for when_pointer_move is
PROCEDURE[ANY, TUPLE[
INTEGER, INTEGER]] (line (2) page 3).
The indirect assignment in line (18)
is thus forbidden according to the previous conformance rules
for TUPLEs seen on
page 9. It may seem nonetheless acceptable, because more parameters
are provided
than required: the print_x method,
when executed, will only use the first
parameter.
Line (19) presents a case similar to that of line (18).
This call is forbidden, but
may be considered useful, with the executed method ignoring optional
information
it does not need.
Conclusion
The above examples clearly show that the conformance rules
presented in this section
are not satisfactory. Some of these examples just point at some
code that could be
accepted but is not validated, thus proving the type system too
cautious. This is
safe, although not desirable for the sake of expressiveness.
However,
a number of examples evidenced true issues, where incorrect code that
is bound to fail is accepted by the type system. This is a major
problem, that led
us to design new, better rules, which are detailed in the next
section.
4 NEW CONFORMANCE RULES
We demonstrated that not having rules for the
special case of ROUTINE types (thus
using the default “generic type” rules), was bound
to raise many problems. In this
chapter we will now present specific rules for the ROUTINE types
and show how
they solve those problems.
New rules definition
Our new conformance rules are specific to agents.
ROUTINE[B1, O1] conforms
to ROUTINE[B2, O2] if
the following two conditions
hold:
- BaseRule: B1 conforms to B2
- OpenRule: O2 conforms to O1
(note the reversed conformance rule)
FUNCTION[B1, O1, R1]
conforms to FUNCTION[B2,
O2, R2] if the following three
conditions hold:
- BaseRule: B1 conforms to B2
- OpenRule: O2 conforms to O1 (note
the reversed conformance rule)
- ResultRule: R1 conforms to R2
Conformance for PROCEDURE[B1,
O1] and PREDICATE[B1, O1] need the conditions
BaseRule and OpenRule as
for the ROUTINE type.
As indicated, our new
conformance rules between O1 and O2 are
the reverse of
what they were in the normal ones (the conformance for generic
types), erroneous
conformance rules presented in section 3. All the other conformance
rules (for basic
types, generic types and TUPLE types) are unchanged and remain
as they were in
section 3.
Note that the previous rules define conformance rules, this
has nothing to do
with covariance or contravariance.
As a summary, a simple way to see
and use these rules is to consider that when
some method requires a formal argument of type ROUTINE[BASE,
OPEN], the
agent which will be provided as effective parameter is bound
to be executed with
arguments conforming to OPEN. So, providing a method able
to handle such arguments
is all that is necessary.
Applying new conformance to examples: issues
are solved
This section details how our new conformance rules impact
all the examples presented
in section 3 and shows how they solve the issues that existed
with the normal
rules.
Let’s start with the same example as the one presented
on page 9:

Line (21) is not valid anymore, because the type of agent
my_dog.eat(?)
is PROCEDURE[DOG, TUPLE[MEAT]] and does not conform to the
type of lunch_action, which is PROCEDURE[ANY,
TUPLE[FOOD]], according to our
new reversed conformance rule OpenRule.
This
code is thus now statically rejected. Line (22) is
still type-valid, but since
line (21) is not, lunch_action may
not be an eat method that requires a MEAT as
argument. Thus, thanks to our new conformance rules, the
invalid code execution
of page 9 is not possible anymore.
The next example considers an agent
as a delayed call, and is based on rewriting
code in different, but equivalent, ways. It was first shown
on page 10.

With
the above definition for T, let’s consider the same code
variants as before:

Now, line (23) is allowed by our new conformance
rules. Indeed, now, PROCEDURE[T, TUPLE[A]]
conforms to PROCEDURE[T, TUPLE[B]],
because
OpenRule requires B to conform to A
(which is trivially true). Line
(24) will execute the f
method with b as an argument, which is valid because f needs
an argument conforming
to A. This safe code is thus accepted now, which increases
expressiveness.
Our third example, first shown on page 11,
consists of the following normally
equivalent code variants:

Now, line (25) is not valid anymore, because OpenRule
states that for PROCEDURE[T, TUPLE[B]]
(the type of agent g) to conform to PROCEDURE[T, TUPLE[a]] (the
type of fa), A must conform to B,
which is of course not true.
This code is thus now correctly rejected. This is a safe
situation, unlike that in
section 3 because as explained there line (26) is
valid and would execute method g with a as an effective argument while expecting a formal
argument of type B. Our
new rules thus prevent the bogus assignment of line (25) that
leads to an invalid situation in line (26).
Our last series of examples, like in section
3, pertains to the conformance of TUPLE types with different number of parameters.
They rely on the code
for the WINDOW class provided on page 3:

On line (27), the type of agent
print_all is PROCEDURE[T, TUPLE[INTEGER,
INTEGER, TIME]]. Since the argument type for when_pointer_move is PROCEDURE
[ANY, TUPLE[INTEGER, INTEGER]] (see line (2) page
3), the indirect
assignment on line (27) is not allowed anymore
according to our new conformance
rules. Indeed, OpenRule requires that TUPLE[INTEGER, INTEGER]]
be conform
to TUPLE[INTEGER, INTEGER, TIME]],
which is not the case according to the
conformance rules between TUPLE types. So this line is now
statically rejected,
which prevents reaching the problem previously explained
on page 12.
On line (28), the type of agent
print_x is PROCEDURE[T, TUPLE[INTEGER]],
and the argument type for when_pointer_move is
PROCEDURE[ANY, TUPLE[INTEGER, INTEGER]] (line (2) page 3).
The indirect assignment on line (28) is
now valid according
to our new conformance rules that require TUPLE[INTEGER,
INTEGER]]
to conform to TUPLE[INTEGER], which is the case. When executed,
the print_x method just
ignores the extra available data. This gives additional
expressiveness,
compared to the normal rules, with total safety.
Line(29) is a case
similar to that of line (28). This call is
now valid as well,
and safe. The executed method simply ignores all data, since
it does not need any.
This capability to ignore some arguments
might seem dangerous if it were allowed
with immediate calls, but we think it is quite useful in
the agent case for at least
three reasons.
First, when working with a graphical system, it is easy
to trace events as shown
in our examples by just printing a message and ignoring all
other data provided with
this event. This is a very simple but convenient way to debug
event-based systems.
Second, some data may be irrelevant for
the action to execute. As an example,
the method to execute when the user clicks on some button
is in most cases independent
from the mouse pointer coordinates when the click is performed.
Our new
rules allow to reuse existing methods not specific to such
an event coming from the
graphical system and which did not care about the mouse
pointer. We think that
such cases are common in practice.
Finally, the ability to take into
account only some of the arguments helps software
evolutions. For example, a graphical system may evolve by
providing more
informations with the ‘button clicked’ event, say,
by adding a time-stamp and a
keyboard status. These extra pieces of information shall
simply be ignored by any
existing code, instead of breaking it all.
Conclusion
All these examples, that revealed problems with the normal
conformance rules for
agents in section 3, now work as expected with our new conformance
rules.
Code that was needlessly rejected with the normal rules
is now accepted, thus
giving extra expressiveness to agents and their users, while
maintaining security
both at compile time (thanks to the type system) and at execution
time.
This is an nice gain, but not the main one. Indeed,
much more important is
the fact that our new rules catch, at compile time, fatal
errors that were completely
undetected with the normal conformance rules. Hence erroneous
code that was
accepted, compiled and crashed at execution is now rejected.
Our rules are thus
safe, while the old ones were not.
5 CONCLUSION
In this paper, we showed that using generic types conformance
rules with ROUTINE types was flawed. We
provided examples to demonstrate that these rules could lead
acceptable and safe code to be unnecessarily rejected, while
erroneous code that had
no way to work properly would be accepted.
To solve these issues, we
added new specific conformance rules that allow agent
assignment to become safe and accept more valid code as well.
These specific rules
do not change the typing of agent per se, but simply conformance
on the second
generic argument (relative to open arguments).
At first sight, these
rules may seem a bit unintuitive to Eiffel developers. But
they provide not only safety, but also extra expressiveness
in a very natural and
useable way, thus making it possible to develop with agents
safely and easily.
To make the agents conformance rule easy
for the user, he has to consider that
if some method needs argument whose type is ROUTINE[BASE,
OPEN], then the
agent he will give as parameter is sure to be called with
arguments conforming to OPEN, and then he needs to give an agent able to
handle such arguments.
To summarize, the main arguments to
adopt these specific
conformance rules are:
- agents are not to be a way to defeat the typing system,
- conformance rules are maintained when the routine is executed,
- newly valid cases are useful and will be executed easily
and safely,
- newly forbidden cases must be so, since they are error
cases,
- if we consider agents as delayed calls, our new rules make
it possible to delay
any call that is valid as an immediate call; valid
delayed calls using all
parameters are also valid as immediate calls.
Thanks to the TUPLE type
conformance rule and new ROUTINE conformance
rule, delayed calls have one more capability than
immediate calls: they may ignore
some arguments. As explained, this property helps
event debugging, software reuse
and software evolutivity.
Footnotes
1 Previously named SmallEiffel.
2 all arguments are open, it is
a shortcut for agent print_all(?, ?, ?)
REFERENCES
[DHM+99] Paul Dubois, Mark Howard, Bertrand Meyer,
Michael Schweitzer, and
Emmanuel Stapf. "From calls to agents". Journal
of Object-Oriented
Programming (JOOP), 12(6), June 1999.
[Mey92] Bertrand Meyer. Eiffel,
The Language. Prentice Hall, Englewood Cliffs,
1992. ISBN 0-13-247925-7.
[Mey00] Bertrand Meyer. "Agents, iteration
and introspection". Chapter 25 of
ongoing work for the new Eiffel, The
Language manual, May 2000.
http://archive.eiffel.com/doc/manuals/language/agent/agent.pdf.
About the authors
Philippe Ribet is software engineer in Toulouse, France. He works on graphic
library design, using Eiffel’s language power and works on SmartEiffel
project. He
can be reached at p.ribet@worldonline.fr.
Cyril Adrian is software engineer in Montb´eliard, France. He joined
the Smart-Eiffel team in summer 2002, working on the project in his
free time. He developed
a new installer, added the Acyclic Visitor design pattern, and worked
on the first
implementation of SCOOP. He can be reached at cyril.adrian@laposte.net.
See also http://www.chez.com/cadrian/.
Olivier Zendra is a Researcher at INRIA-Lorraine
/ LORIA in Nancy, France. He
works on the definition, compilation and optimization of object-oriented
languages
and on the SmartEiffel project. He can be reached at Olivier.Zendra@loria.fr.
See
also http://www.loria.fr/˜zendra.
Dominique Colnet is Professor at the
University of Nancy. He is the original
author of the GNU Eiffel compiler and the leader of the SmartEiffel
team. He can
be reached at Dominique.Colnet@loria.fr.
See also http://www.loria.fr/ ˜colnet.
Cite this article as follows: Philippe Ribet, Cyril Adrian, Olivier
Zendra, Dominique Colnet:
"Conformance of agents in the Eiffel language", in Journal
of Object Technology, vol. 3, no. 4, April 2004, Special issue:
TOOLS USA 2003, pp. 125-143. http://www.jot.fm/issues/issues_2004_04/article7
|