ET: Agents

Our last mechanism, agents, adds one final level of expressive power to the framework describe so far. Agents apply object-oriented concepts to the modeling of operations.

Objects for operations

Operations are not objects; in fact, object technology starts from the decision to separate these two aspects, and to choose object types, rather than the operations, as the basis for modular organization of a system, attaching each operation to the resulting modules -- the classes.

In a number of applications, however, we may need objects that represent operations, so that we can include them in object structures that some other piece of the software will later traverse to uncover the operations and, usually, execute them. Such "operation wrapper" objects, called agents, are useful in a number of application areas such as:

  • GUI (Graphical User Interface) programming, where we may associate an agent with a certain event of the interface, such as a mouse click at a certain place on the screen, to prescribe that if the event occurs -- a user clicks there -- it must cause execution of the agent's associated operation.
  • Iteration on data structures, where we may define a general-purpose routine that can apply an arbitrary operation to all the elements of a structure such as a list; to specify a particular operation to iterate, we will pass to the iteration mechanism an agent representing that operation.
  • Numerical computation, where we may define a routine that computes the integral of any applicable function on any applicable interval; to represent that function and pass its representation to the integration routine, we will use an agent.

Operations in Eiffel are expressed as routines, and indeed every agent will have an associated routine. Remember, however, that the fundamental distinction between objects and operations remains: an agent is an object, and it is not a routine; it represents a routine. As further evidence that this is a proper data abstraction, note that the procedure call, available on all agents to call the associated routine, is only one of the features of agents. Other features may denote properties such as the class to which the routine belongs, its precondition and postcondition, the result of the last call for a function, the number of arguments.

Building an agent

In the simplest form, also one of the most common, you obtain an agent just by writing agent r

where r is the name of a routine of the enclosing class. This is an expression, which you may assign to a writable entity, or pass as argument to a routine. Here for example is how you will specify event handling in the style of the EiffelVision 2 GUI library: your_icon.click_actions.extend (agent your_routine)

This adds to the end of your_icon.click_actions -- the list of agents associated with the "click" event for your_icon, denoting an icon in the application's user interface -- an agent representing your_routine. Then when a user clicks on the associated icon at execution, the EiffelVision 2 mechanisms will call the procedure call on every agent of the list, which for this agent will execute your_routine. This is a simple way to associate elements of your application, more precisely its "business model" (the processing that you have defined, directly connected to the application's business domain), with elements of its GUI.

Similarly although in a completely different area, you may request the integration of a function your_function over the interval 0..1 through a call such as your_integrator.integral (agent your_function, 0, 1)

In the third example area cited above, you may call an iterator of EiffelBase through your_list.do_all (agent your_proc)

with your_list of a type such as LIST [YOUR_TYPE]. This will apply your_proc to every element of the list in turn.

The agent mechanism is type-checked like the rest of Eiffel; so the last example is valid if and only if your_proc is a procedure with one argument of type YOUR_TYPE.

Operations on agents

An agent agent r built from a procedure r is of type PROCEDURE [T, ARGS] where T represents the class to which r belongs and ARGS the type of its arguments. If r is a function of result type RES, the type is FUNCTION [T, ARGS, RES]. Classes PROCEDURE and FUNCTION are from the Kernel Library of EiffelBase, both inheriting from ROUTINE [T, ARGS].

Among the features of ROUTINE and its descendants the most important are call, already noted, which calls the associated routine, and item, appearing only in FUNCTION and yielding the result of the associated function, which it obtains by calling call.

As an example of using these mechanisms, here is how the function integral could look like in our INTEGRATOR example class. The details of the integration algorithm (straight forward, and making no claims to numerical sophistication) do not matter, but you see the place were we evaluate the mathematical function associated with f, by calling item on f: integral (f: FUNCTION [TUPLE [REAL], REAL]; low, high: REAL): REAL -- Integral of `f' over the interval [`low', `high'] require meaningful_interval: low <= high local x: REAL do from x := low invariant x >= low x <= high + step -- Result approximates the integral over -- the interval [low, low.max (x - step)] until x > high loop Result := Result + step * f.item ([x]) -- Here item is applied to f x := x + step end end

Function integral takes three arguments: the agent f representing the function to be integrated, and the two interval bounds. When we need to evaluate that function for the value x, in the line Result := Result + step * f.item ([x])

we don't directly pass x to item; instead, we pass a one-element tuple [x], using the syntax for manifest tuples introduced in "Tuple types" . You will always use tuples for the argument to call and item, because these features must be applicable to any routine, and so cannot rely on a fixed number of arguments. Instead they take a single tuple intended to contain all the arguments. This property is reflected in the type of the second actual generic parameter to f, corresponding to ARGS (the formal generic parameter of FUNCTION): here it's TUPLE [REAL] to require an argument such as [x], where x is of type REAL.

Similarly, consider the agent that the call seen above: your_icon.click_actions.extend (agent your_routine)

added to an EiffelVision list. When the EiffelVision mechanism detects a mouse click event, it will apply to each element item of the list of agents, your_icon.click_actions, an instruction such as item.call ([x, y])

where x and y are the coordinates of the mouse clicking position. If item denotes the list element agent your_routine, inserted by the above call to extend, the effect will be the same as that of calling your_routine (x, y)

assuming that your_routine indeed takes arguments of the appropriate type, here INTEGER representing a coordinate in pixels. (Otherwise type checking would have rejected the call to extend.)

Open and closed arguments

In the examples so far, execution of the agent's associated routine, through item or call, passed exactly the arguments that a direct call to the routine would expect. You can have more flexibility. In particular, you may build an agent from a routine with more arguments than expected in the final call, and you may set the values of some arguments at the time you define the agent.

Assume for example that a cartographical application lets a user record the location of a city by clicking on the corresponding position on the map. The application may do this through a procedure record_city (cn: STRING; pop: INTEGER; x, y: INTEGER) -- Record that the city of name `cn' is at coordinates -- `x' and `y' with population `pop'.

Then you can associate it with the GUI through a call such as map.click_actions.extend (agent record_city (name, population, ?, ?))

assuming that the information on the name and the population has already been determined. What the agent denotes is the same as agent your_routine as given before, where your_routine would be a fictitious two-argument routine obtained from record_city -- a four-argument routine -- by setting the first two arguments once and for all to the values given, name and population.

In the agent agent record_city (name, population, ?, ?), we say that these first two arguments, with their set values, are closed; the last two are open. The question mark syntax introduced by this example may only appear in agent expressions; it denotes open arguments. This means, by the way, that you may view the basic form used in the preceding examples, agent your_routine, as an abbreviation -- assuming your_routine has two arguments -- for agent your_routine (?, ?). It is indeed permitted, to define an agent with all arguments open, to omit the argument list altogether; no ambiguity may result.

For type checking, agent record_city (name, population, ?, ?) and agent your_routine (?, ?) are acceptable in exactly the same situations, since both represent routines with two arguments. The type of both is PROCEDURE [TUPLE [INTEGER, INTEGER]]

where the tuple type specifies the open operands.

A completely closed agent, such as agent your_routine (25, 32) or agent record_city (name, population, 25, 32), has the type TUPLE, with no parameters; you will call it with call ([ ]), using an empty tuple as argument.

The freedom to start from a routine with an arbitrary number of arguments, and choose which ones you want to close and which ones to leave open, provides a good part of the attraction of the agent mechanism. It means in particular that in GUI applications you can limit to the strict minimum the "glue" code (sometimes called the controller in the so-called MVC, Model-View Controller, scheme of GUI design) between the user interface and "business model" parts of a system. A routine such as record_city is a typical example of an element of the business model, uninfluenced -- as it should be -- by considerations of user interface design. Yet by passing it in the form of an agent with partially open and partially closed arguments, you may be able to use it directly in the GUI, as shown above, without any "controller" code.

As another example of the mechanism's versatility, we saw above an integral function that could integrate a function of one variable over an interval, as in your_integrator.integral (agent your_function, 0, 1)

Now assume that function3 takes three arguments. To integrate function3 with two arguments fixed, you don't need a new integral function; just use the same integral as before, judiciously selecting what to close and what to leave open: your_integrator.integral (agent function3 (3.5, ?, 6.0), 0, 1)

Open targets

All the agent examples seen so far were based on routines of the enclosing class. This is not required. Feature calls, as you remember, were either unqualified, as in f (x, y), or qualified, as in a.g (x, y). Agents, too, have a qualified variant as in agent a.g

which is closed on its target a and open on the arguments. Variants such as agent a.g (x, y), all closed, and agent a.g (?, y), open on one argument, are all valid.

You may also want to make the target open. The question mark syntax could not work here, since it wouldn't tell us the class to which feature g belongs, known in the preceding examples from the type of a. As in creation expressions, we must list the type explicitly; the convention is the same: write the types in braces, as in agent {SOME_TYPE}.g agent {SOME_TYPE}.g (?, ?) agent {SOME_TYPE}.g (?, y)

The first two of these examples are open on the target and both operands; they mean the same. The third is closed on one argument, open on the other and on the target.

These possibilities give even more flexibility to the mechanism because they mean that an operation that needs agents with certain arguments open doesn't care whether they come from an argument or an operand of the original routine. This is particularly useful for iterators and means that if you have two lists your_account_list: LIST [ACCOUNT] your_integer_list: LIST [INTEGER]

you may write both your_account_list.do_all (agent {ACCOUNT}.deposit_one_grand) your_integer_list.do_all (agent add_to_total)

even though the two procedures used in the agents have quite different forms. We are assuming here that the first one, a feature of class ACCOUNT, is something like deposit_one_grand -- Deposit one thousand into `Current'. do deposit (1000) end

The procedure deposit_one_grand takes no arguments. In the do_all example above, its target is open. The target will be, in turn, each instance of ACCOUNT in your_account_list.

In contrast, the other routine, assumed to be a feature of the calling class, does take an argument x: add_to_total (x: INTEGER) -- Add `x' to the value of `total'. do total := total + x end

Here, total is assumed to be an integer attribute of the enclosing class. In the do_all example, each instance of your_integer_list will fill the argument x left open in add_to_total.

Without the versatility of playing with open and closed arguments for both the original arguments and target, you would have to write separate iteration mechanisms for these two cases. Here you can use a single iteration routine of LIST and similar classes of EiffelBase, do_all, for both purposes:

  • Depositing money on every account in a list of accounts.
  • Adding all the integers in a list of integers.

Inline agents

In the agent discussion above, it has been assumed that there already exists some routine that we wish to represent with an agent. However, sometimes the only usage of such a routine could be as an agent ... that is, the routine does not make sense as a feature of the class in question. In these cases, we can use inline agents. With an inline agent we write the routine within the agent declaration.

If we consider the use of agents instead of class features in the two do_all examples in the previous section, the agents would be coded as follows:

your_account_list.do_all (agent (a: ACCOUNT) do a.deposit (1000) end)

and

your_integer_list.do_all (agent (i: INTEGER) do total := total + i end)

The syntax of the inline agent corresponds to the syntax of a routine. Immediately following the agent keyword are the formal arguments and in the case of functions the type for Result. Inline agents can have local entities, preconditions, and postconditions, just like any routine.

Inline agents do not have access to the local entities of the routine in which they are coded. So, if it is necessary to use the routine's local variables, they must be passed as arguments to the inline agent.

Here's an example of an inline agent which is a function. It is used in the context of a check to see if every element of your_integer_list is positive:

your_integer_list.for_all (agent (i: INTEGER): BOOLEAN do Result := (i > 0) ensure definition: Result = (i > 0) end)

Inline agents are interesting also as an implementation of the notion of closures in computer science.

Agents provide a welcome complement to the other mechanisms of Eiffel. They do not conflict with them but, when appropriate -- as in the examples sketched in this section -- provide clear and expressive programming schemes, superior to the alternatives.

Compatibility note: earlier versions of the agent classes (ROUTINE, PROCEDURE, FUNCTION, PREDICATE) had an extra initial generic parameter, for which ANY was generally used. The compiler has been engineered to accept the old style in most cases.

See Also: Event Programming with Agents

cached: 03/19/2024 12:32:42.000 AM