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 .
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.
In the simplest form, also one of the most common, you obtain an agent just by writing
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:
This adds to the end of my_icon . click_actions -- the list of agents associated with the "click" event for my_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
In the third example area cited above, you may call an iterator of EiffelBase through
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 .
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 (straightforward, and making no claims to numerical sophistication) do not matter, but you see, in the highlighted line, the place were we evaluate the mathematical function associated with f , by calling item on f :
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
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", page 90 . 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:
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
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
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 .)
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 ; x, y : INTEGER; pop : INTEGER ) |
Then you can associate it with the GUI through a call such as
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
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
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:
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
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
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
even though the two procedures used in the agents have quite different forms. We are assuming here that the first one, in class ACCOUNT , is something like
so that it doesn't take an argument: it is normally called on its target, as in my_account . deposit_one_grand. In contrast, the other routine has an argument:
where total is an integer attribute of the enclosing class. 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:
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.
Eiffel Home Page (Web) -- Getting started with Eiffel (local)
Copyright Interactive Software Engineering, 2001