Event Programming with Agents

In Eiffel there is a facility referred to as agents.

The implementation of agents is an advanced topic, but you do not have to understand the details of the implementation of agents to put agents to work for you. That is what you will learn in this section.

Objects that Represent Operations

Object technology is based on the idea that when we model systems based on objects, representing the "things" they manipulate. As to operations on these objects, they appear in the corresponding classes, as routines (functions and procedures). Operations are not objects.

Sometimes, on the other hand, the "things" we model with our objects could represent operations. For example, we might want to build a list of tasks to be performed later; each task is defined by a routine. Each of the objects in the list will represent the corresponding routine.

Such an object, representing an operation, is called an agent.

If we can have a run-time object that represents an operation, then we can place the object in the structure of another object, where at some later time, a client can cause the associated operation to execute.

This is a very desirable model for event driven processing, like graphical user interfaces. The operations that are executed when a user take some action like clicking on a button, could be represented by agents. When the user interface element is initialized, agents that represent the action routines are stored within the interface element. Then at the time that an event, say a button click, occurs, the agents for that event are retrieved and their associated operations are executed.

Another area in which agents are commonly used is in traversing data structures. Many of the data structure classes in the Base Library include routines which take agents as there arguments. For example, the feature do_all takes an agent which represents some procedure and will apply the procedure to every item in the structure.

Classes to Model Operations

We know that there are two types of routines in Eiffel, functions and procedures.

The implementation of agents correspondingly relies on three classes in the Base Library: class ROUTINE for the general notion, and its heirs FUNCTION, with and PROCEDURE. In addition, PREDICATE, an heir of FUNCTION , covers the particular case of a function returning a boolean result.

When you use an agent from a client routine, you will be building an instance of either FUNCTION or ROUTINE.

Using Agents

Below is an instruction which passes an agent as an argument to a procedure. button.select_actions.extend (agent gauge.step_forward)

In this example, the producer wants to add the action of stepping the gauge forward in the event that a button is clicked. The keyword "agent" is used to indicate that at runtime an object of type PROCEDURE should be created which represents applying the feature step_forward to the object attached to gauge. It is the object of type PROCEDURE that is passed as the argument.

It is important to understand that step_forward does not get applied at the point that the instruction above is executed. Rather the procedure object that represents step_forward is given to the button to hold in reserve. Then at the point that the button click event takes place, the button will go through its list of select_actions executing their associated routines. Only then does step_forwardget applied to gauge.

Agents with Arguments

In this example, the routine "step_forward" on which the agent is based takes no arguments. If you drilled down into the workings of this example you would find that class that implements the feature extend is class EV_NOTIFY_ACTION_SEQUENCE. You would also see that the signature for the feature extend is as essentially as follows. extend (v: PROCEDURE [TUPLE])

We don't have to know too much about the workings of agents to see that "extend" takes an argument v which is of type PROCEDURE. The actual generic parameter TUPLE represents the set of "open" arguments. In this case, extend is expecting an agent with no open arguments.

Open and Closed Arguments

It is this business of open and closed arguments which really makes agents remarkable. To get a feel for it, let's simplify the example some. Instead of considering an agent passed as an argument let's look at it as a simple assignment within a class.

Suppose a class has a feature declared as shown below. my_procedure: PROCEDURE [TUPLE]

Then what can be assigned to my_procedure?. An agent, of course. Say the class has procedures as follows. no_argument_procedure -- A procedure with no arguments do print ("No argument here!%N") end two_argument_procedure (an_int: INTEGER; another_int: INTEGER) -- A procedure with two arguments do print ("My arguments are: " + an_int.out + " and " + another_int.out + "%N") end

Then the following assignment is valid. my_procedure := agent no_argument_procedure

What this means is that the agent created and associated with the procedure no_argument_procedure must conform to the type PROCEDURE [TUPLE]. The feature my_procedure (which is of type PROCEDURE [TUPLE]) can be attached at runtime to an agent representing a procedure with no open arguments, which indeed is what no_argument_procedure is.

Now let's turn our attention to the other procedure two_argument_procedure. You might think that because it takes two arguments, that you would not be able to build an agent from it which could be assigned to the attribute my_procedure. But you can do it by closing the two arguments at the time that the agent is created, as in the following. my_procedure := agent two_argument_procedure (1, 2) -- Is Valid

What happens here is that values are fixed for those arguments at the time that the agent, an object of type PROCEDURE [ TUPLE] is created.

So this is the wonderful thing about agents. A routine which will be represented as an agent does not have to be an exact fit for the expected signature. By closing some arguments at agent creation, you have effectively produced a new and conforming routine.

The advantage of this is that you can sometimes avoid building specialized routines for the sole purpose of having a routine which conforms to the agent signature.

To leave an argument open, you hold its place with a question mark. If you intend for all arguments to be open, then you may make them all question marks, or leave off the arguments entirely. my_procedure := agent two_argument_procedure (?, 2) -- Argument 1 left open my_procedure := agent two_argument_procedure (?, ?) -- Both arguments left open my_procedure := agent two_argument_procedure -- Both arguments left open

If an argument is open, then it means that a value is not provided for that argument at the time that the agent is created. The implication is that the value must be provided at some time prior to the time that the agent's associated routine gets executed. A precondition to executing a routine associated with an agent is that the agent has a valid set of arguments (called operands within the ROUTINE classes) for the call. If you were to leave one or both of the arguments to two_argument_procedure open as in the examples above, the assignment would still work due to the rules governing TUPLE conformance. But, at runtime unless the other arguments had been provided, the "valid operands" precondition would be violated.

Let's see an example in which we leave a target open. Suppose we have a class that has a feature coded as below my_strings: LINKED_LIST [STRING]

and some code to put some strings in my_strings: create my_things.make my_strings.extend ("Hello") my_strings.extend ("World!")

Our class also has a feature called print_on_new_line which we created to print a string preceded by a new line character. print_on_new_line (s: STRING) -- Print `s' preceded by a new line do print ("%N" + s) end

Now suppose we want to print the values of all the strings in my_strings each on a separate line by invoking print_on_new_line. Traditionally, we would do it by traversing the LINKED_LIST and printing each item. Like this: from my_list.start until my_list.exhausted loop print_on_new_line (my_list.item) my_list.forth end

The availability of agents gives us new options. LINKED_LIST has a feature do_all which comes to it from its ancestor LINEAR. The do_all feature's signature looks like this: do_all (action: PROCEDURE [TUPLE [G]])

As an argument do_all takes an agent based on a procedure with one open argument which is the same type as the list items (in this class, G is the formal generic parameter representing the type of the items being stored). Then it traverses the list executing the routine associated with that agent and uses the current list item to satisfy the open argument.

Instead of coding the loop shown above, we can code this instruction: my_list.do_all (agent print_on_new_line (?))

we leave open the argument required by print, and do_all will provide it as a reference to the current list item as it traverses the list.

Targets for Agents' Routines

In Eiffel every routine must be applied against a target object. In our model for computation, x.f (a, ...), the x is the target of the application of feature f. In the case of an agent, the agent must account for objects for each of the arguments and an object for the target of the routine.

Let's identify the targets in the examples shown. First: button.select_actions.extend (agent gauge.step_forward)

Here the target is the object attached to the entity "gauge" which is (although you cannot determine it from this line taken out of context) an object of type EV_GAUGE.

How about this: my_procedure := agent two_argument_procedure (1, 2)

Here, since there was no qualification, then the target is the current instance. Same with this: my_list.do_all (agent print_on_new_line (?))

Again, consider the fact that the agent must account for objects for each of the arguments to a routine, and an object for the target. So, in the examples we've seen so far, the target is close, that is provided at the time of the creation of the agent.

But we can actually leave the target open as well. Now we cannot use the question mark notation to do that, because if we did, there would be no way to know of which class the routine is a feature. So instead, we mark an open target with the class name in braces.

Suppose in our list of strings example, we wanted to print the strings, then convert them to lower case, then print them again. Remember that "do_all" has one open argument, which will be provided as the current list item during the traversal. my_list.do_all (agent print_on_new_line (?)) my_list.do_all (agent {STRING}.to_lower) my_list.do_all (agent print_on_new_line (?))

In between printing the list two times, we provide do_all with an agent that representing the STRING class's feature to_lower which will convert each string in the list to lower case. Notice that to_lower does not take an argument of type STRING as print_on_new_line did. Rather it gets applied to an instance of STRING, so it is targeted to a string. So we leave its target open and do_all provides the current list item as the target.

Agents for Functions

So far all the agents that we have coded have created instances of PROCEDURE. But functions are routines and can be represented as agents as well. The difference is that functions have a return value.

Let's extend the string example by using an agent that represents a function. Suppose we wanted to print only those strings which contain a particular character, say the exclamation point.

Here again we'll use a feature of the LINKED_LIST class. There is a feature called do_if which takes two agents as arguments. One is an action procedure like the argument that do_all takes, and the other is a function which returns a boolean and used as a test. As each list item is current, the test is applied first. If the result is true, then the action is applied with the current item. my_list.do_if (agent print_on_new_line(?), agent {STRING}.has('!'))

The agent for the action is the same as we used earlier. We've added an agent for the test. It represents applying the has feature of the STRING class. Here the target is left open, because we want each of the strings in the list to be the target of has.

Compatibility note

Versions of the Kernel Library classes ROUTINE, PROCEDURE, FUNCTION and PREDICATE prior to EiffelStudio 17-05 had an extra generic parameter at the initial position; the usual actual generic parameter was ANY. It has been removed. The compiler has been engineered so that in almost all cases it will still accept the old style.

cached: 03/19/2024 3:33:28.000 AM