The structure of actors Actors perform their computation in a sequence of steps called firings. In each such step, an actor may: • Consume tokens from its input ports • Modify its internal state • Produce tokens at its output ports Consequently, describing an actor involves describing its interface to the outside and the ports, the structure of its internal state, and the steps it can perform, what these steps do (in token production and consumption, and the update of actor state), and how to pick the step that an actor will
perform next. This section discusses some of the constructs in the CAL language that deal with these issues.
Actions describe the things that occur during a step that an actor takes. It is accurate to say that a step consists of executing an action. When an actor takes a step, it may consume input tokens and produce output tokens. Therefore, input patterns do these: • Define the number of tokens (for each port) to be consumed when an action is executed (fired). • Declare the variable symbols by which tokens consumed by an action firing are referred to within an action. • Define a firing condition for an action, i.e., a condition that must be met for the action to be able to fire. The output side of an action is a little simpler, the output expressions simply define the number and values of the output tokens to be produced on each output port by each firing of the action. It is permissible to omit the explicit naming of the port that an input pattern or output expression applies to if an action provides as many input patterns as there are input ports, or output expressions as there are output ports. In such a case, the patterns or expressions are matched by position against the port declarations. One way to view an actor is as an
operator on streams of data – sequences of tokens enter it on its input ports, and sequences of tokens leave it on its output ports. When discussing the operation of an actor, it is often useful to look at it as an operator on streams. Actors can have
parameters. They act as constants during the actor execution, and are given a concrete value when an actor is instantiated as part of an actor network. The main purpose of actor parameters is to allow programmers to specify families of related actors, without having to duplicate a lot of code.
Non-determinism A
non-deterministic actor is one that, for the same input sequences, allows more than one run and more than one possible output. Non-determinism can be very powerful when used appropriately, but it can also be a very troublesome source of errors. One concern is that non-determinism might be introduced into an actor inadvertently, i.e., an author thinks an actor is deterministic even if it is not. One of the key design goals of the CAL language was to allow the description of non-deterministic actors, while simultaneously permitting tools to identify possible sources of non-determinism, so that they can
warn about them. A key consequence of a non-deterministic actor like
NDMerge is that during an execution, its output may depend on the timing of its input. If both its input queues are empty, and
NDMerge is waiting for input, then whatever input the next token arrives at may be the one that is copied next to the output. Consequently, the scheduling of activities in the actor network, or the relative speeds of the actors feeding into an actor like
NDMerge may affect the output of the system. This may, occasionally, be desirable, and at other times it may not. Regardless, it is a property that must be considered. One way to view non-determinism of the kind that makes an actor dependent on the precise timing of token arrivals is that such an actor only appears to be non-deterministic if viewing it as an operator on streams, because that view abstracts from the temporal properties of the execution, and thus purposefully removes information that is used to determine the sequence in which actions fire. From the perspective of the CAL language, this is not entirely accurate, but even so, it is easy to write non-deterministic actors that would not be deterministic even if everything is known about the timing of the tokens and the actor implementation, such as:
Guarded actions The guard clause of an action contains a set of expressions that all must be true for the action to be fireable. For the first action to be fireable, the incoming token must be greater or equal to zero, in which case it will be sent to output
P. Otherwise that action cannot fire. Conversely, for the second action to be fireable, the token must be less than zero, in which case it is sent to output N. A run of this actor might look like this: An actor could run into trouble if it ever encounters a zero token, because none of its actions will be able to fire on it. It's not illegal to write actors that terminate on some input, and it may be important to have a few of those in some systems. But it is a pitfall that can cause problems. Secondly, the guard conditions are also disjoint along with being exhaustive. Finally, guard conditions can
peek at incoming tokens without consuming them. If the guards are false or the action is not fired for some other reason, and if the token is not consumed by another action, then it remains where it is, and is available for the next firing. (Or it can remain there indefinitely, as in the case of the zero token in front of
SplitDead, which is never removed because the actor is dead.) The
Select actor below is another example of the use of guarded actions. It is similar to the
NDMerge actor in the sense that it merges two streams (the ones arriving at its A and B input ports). However, it does so according to the (Boolean) values of the tokens arriving at its
S input port.
Actors with state In all the actors so far, nothing an action firing did would in any way affect subsequent firings of actions of the same actor. Using
state variables, action firings can leave information behind for subsequent firings of either the same or a different action of the same actor. The way this actor is written, selecting the next input token and then copying the token to the output is one atomic step.
Select and
IterSelect are almost, but not entirely, equivalent. Firstly,
IterSelect makes twice as many steps to process the same number of tokens. Secondly, it reads, and therefore consumes, the
S input token, irrespective of whether a matching data token is available on
A or
B. ==Schedules==