Actors (objects)

melophone

 
 

The notion of an object is now widespead in programming. This concept is used to organize code by gathering values together into a state and making the possible interactions with this state explicit through the notion of methods.

A related and less popular notion is the concept of an actor. The actor model of programming was developed in the begining of the ’70s with the work of Carl Hewitt and languages like Act. Later, Actor programming languages included the Ptolemy programming language and languages offering “parallel objects” like Scala or Erlang.

While objects focus on reuse with mechanisms like inheritance, method subtyping, state hidding, etc., actors focus on the management of concurrent activities of autonomous entities.

In Antescofo we use the words object or actor interchangably to refer to some kind of process used to encapsulate a state and to the concurrent, parallel and timed interactions with this state. The specification of these objects and the interactions with them are supported by some specific syntactic constructs. However, these dedicated constructions are internaly rewritten in more fundamental mechanims of functions, processes and whenever statements.

This chapter supposes a knowledge of the notions of objects and methods. The next section compares the notion of object with the notion of process. Then we describe the Antescofo notion of actors.

Introduction: Process as Object

A process instance can be used as a kind of autonomous entity encapsulating some data. In fact, a running process can be seen as an object or as an actor:

  • a process instance is similar to the instance of a class: the process is the class and calling a process corresponds to class instantiation;

  • the interval of time between the process call and the process end corresponds to the lifetime of the object;

  • the exe of the instance corresponds to a reference to the object;

  • the state of the object corresponds to the values of the local variables of the process instance;

  • interactions with the object can be achieved by assigning its local variable (see dot notation).

Local variable assignments act as messages and in response to a message that it receives, a running process can make local decisions, create more processes, send more messages, and determine how to respond to the next message received.

For example, suppose we want to design an object of a class channel supposed to control some audio channel. The object iterates periodically over a list of parameters to be sent to a harmonizer. During the object lifetime, it is possible to add a new parameter and to reset the parameter list. We can implement this notion through a process ::channel. By assigning the local variable $velocity, a new parameter is added to the list. By assigning it to 0, this list is reset to its initial value.

          @proc_def ::channel($ch, $init, $period)
          {
               @local $velocity, $tab, $i
               $velocity := 0
               $tab := $init
               $i := 0

               whenever ($velocity == 0)
               { $tab := init }

               whenever ($velocity != 0)
               { _ := @push_back($tab, $velocity) }

               Loop $period
               { 
                    $i := ($i + 1) % @size($tab)
                    @command{ "harmo" ++ $ch} ($tab[$i])
               }
          }
          ; ...

          $p0 := ::channel(0, [12, 15], 1)
          $p1 := ::channel(1, [10, 8, 9, 11], 1.5)
          ; ...

          $p0.$velocity := 7
          ; ...

The dot notation is efficient but does not make the interactions with the object (running process) apparent. Functions can be used to make these interactions more explicit. Suppose we want to simultaneously change the period and reset the parameter list to its initial value. We can write a function:

          @fun_def reset($pid, $per)
          {
              $pid.$period := $per
              $pid.$velocity := 0
          }

then we can call the function :

          @reset($pid, 1.5)

and using the infix notation for function calls introduced in section Infix Notation for Function Calls:

          $pid.reset(1.5)

This last form is in line with the usual notation used to call an object’s method: we ask the object (specified through its exe $pid) to perform the method with parameter 1.5.

Actors

The previous idea — relying on processes to achieve a kind of concurrent object oriented programming — is pushed further with the @obj_def construction. An @obj_def definition is internally expanded into a process definition and into function definitions following the line sketched by the previous example. So, there is no fundamentally new mechanism involved. However, the dedicated syntax makes the programming more readable and reusable.

An obj definition is introduced by the keyword @obj_def and consists in a sequence of clauses. The order of the clause may be relevant. A clause of a given type may appear several times in an object definition. There are 8 kind of clauses, introduced by a keyword:

  • @local introduces the declaration of the fields (also known as the attribute) of the object.

  • @init defines a sequence of actions that will be launched at object instantiation.

  • @method_def or @fun_def specifies a new method, i.e. a function that can be run on a specific obj. Such method are also named instance method or object method because they involve a specific instance of an object. The body of a method is an extended expression.

  • @proc_def specifies a new method, which is similar to the previous construction, except that the body of a routine is a sequence of actions (not an extended expression). These methods are sometimes called routines. They can have a duration and multiple instance of the same routine can be simultaneously active for the same object.

  • @broadcast declares a function that performs simultaneously on all instances of an object.

  • @whenever introduce a daemon which triggers a sequence of actions when some logical expression becomes true.

  • @react is similar to the previous construct but triggers the evaluation of an extented expression when some logical expression becomes true.

  • @abort defines an abort handler that will be triggered when the object is killed.

An object definition plays a role similar to a class in object-oriented programming, except that there is no notion of class inheritance in the current Antescofo version. Another difference is that objects run “in parallel” and their actions are subject to synchronization with the musician or on a variable, they can be performed on a given tempo, etc. As a matter of fact, as previously mentioned, objects are processes with some syntactic sugar.

(click here for a larger view)

After a motivating example, we will detail the various clauses of an object definition.

A Basic Example

Here is a first example of an object definition:

          @obj_def  Metro($p, $receiver)
          {
               @local $period, $trigger, $body

               @init { 
                 $trigger := false 
                 $body := 0
               }

               @whenever ($trigger) 
               {
                   $body := { Loop $period { @command($receiver) TOP } }
               }

               @init {
                 $period := $p
                 $trigger := true
               }

               @broadcast reset()
               { 
                   abort $body
                   $period := $p 
                   $trigger := true
               }

               @method_def current_period() { return $period }
               @method_def set_period($x)   { $period := $x }

               @abort { print "object " $THISOBJ "is killed" }
          }

The object is called obj::Metro and corresponds to a new type of value. This type is a subtype (a specialization) of proc. It can be instantiated by giving the expected arguments for the object creation. These arguments are specified between parentheses after the object's name.

          $metro1 := obj::Metro(2/3, "left_channel")
          $metro2 := obj::Metro(1, "right_channel")

An object of type obj::Metro is created with an initial period. The purpose of this object is to send a message TOP to a receiver each period. The loop implementing the periodic emission of the TOP is triggered by a whenever controlled by a field (a local variable) $trigger. The exec of this loop is saved in field $body and used to abort the loop when the broadcast @reset is emitted.

Two methods are provided: @set_period is used to change the value of the period (the change is taken into account at the end of the current period) and @current_period is used to query the period actually used by a obj::Metro. The broadcast @reset can be used to reset the period of all running instances of a to their initial value (the value given at creation time). Here are some examples:

          _ := $metro1.set_period(2 * $metro2.current_period())

sets the period of the first object to twice the period of the second object. All periods are reset calling the broadcast:

          _ := @reset()

Note that a broadcast corresponds to an ordinary function. This function launches simultaneously, for all active instances, the code associated to the broadcast.

Notice that the definition specifies two @init clauses: the first one takes place before the @whenever and initializes the fields of the object. The second @init clause is used to launch the loop when the object is created and after the start of the @whenever.

An object lives “forever”. It can be killed and the command abort. When killed, the object will execute its abort handler. In the example, the abort handler uses the system variable $THISOBJ that refers, in the scope of an object clause, to the current instance of the object.

In the next paragraphs, we detail the various clauses present in an object definition.

Field Definition: @local

The clause has the same syntax as the declaration used to introduce local variables in a compound action. Here each “local variable” is used as a field of the object and corresponds to a local variable in the process that implements the object. The values of the fields/local variables represents the state of the object. Fields are also called slots or object's members. Fields allow actor to encapsulate data.

Antescofo is a dynamically typed programming language, so the fields of an object have no specified type and can hold any kind of values in the course of time.

Several @local clauses can be defined and their order and placement is meaningless. Object fields are present from the start and initialized with the undef value.

Note that the argument of an object corresponds to implicitly defined fields. So, in the previous example, the state of the object is given by 5 variables: the initial period $p, the receiver $receiver, the current period $period, a control variable $trigger and the exec to the running loop that implements the object behavior $body.

A reference to a field may appear anywhere in a clause and always refers to the corresponding local variable. A variable identifier that is not declared as a local variable, refers to a global variable.

Performing an Action at the Object Construction: @init

Fields are initialized in @init clauses. Init clauses are interleaved with @whenever clauses and this order is preserved in the implementation, which makes possible to control the order of evaluation and the triggering of the whenever clauses.

In our example, the assignment of $trigger in the second @init clause will trigger the @whenever previously defined.

Specifying an Object Method: @method_def and @proc_def

Methods are functions or processes that are associated to an object. They represent some behaviors specified as

  • an expression: such methods are introduced by the keyword @method_def or equivalently by @fun_def (because such methods are similar to functions);

  • or a sequence of actions: such methods are introduced by the keyword @proc_def because such methods are similar to processes). Such methods are called routines when we want to distinguish them from the previous type of methods.

Methods are called on objects and may involve some additional arguments. They have several advantages over bare functions or processes:

  • A method can be overloaded, that is, the same method name can be used for a different object.

  • When called, a method checks implicitly that is is called on a live instance of an object.

  • When called, a method checks implicitly that is is called on an object of the expected type.

  • A method has direct access to the object's fields.

These benefits come at some cost:

  • A method can be called only through the infix call notation. A side consequence is that a method call is an expression (even if the method is a routine).

  • Methods are not values, they are just simple names. For instance, you cannot pass them as arguments1.

Ambiguity Between a Method Call and a Function Call in Infix Form

There is a possible ambiguity between infix function calls and method calls. This ambiguity arises if a function @f is defined and takes a first argument (an object) on which a method f is also defined. Then the call

      obj . f (aᵢ)

is ambiguous: is it a method call to f or a function call @f(obj, aᵢ)?

In this case, the rule is to call the method (if obj is alive). If you want to call the function, use the @-identifier in the call:

      obj . @f (aᵢ)

Calling a Method on a Dead Object

If a method is called on a dead process, Antescofo looks for an ordinary function with the same identifier. If this function exists, it is called with the same arguments, instead of calling the method. If this function does not exist, an error is signaled.

Here is an example

         @obj_def obj::Account()
         {
             @local $deposit
             @init { $deposit := 0 }
             @fun_def credit($x) { $deposit := $deposit + $x }
             @fun_def debit($x) { $deposit := $deposit - $x }
         }

         @fun_def @credit($x)
         { print "cannot credit a non-longer existant account" }

         ; ...
         $joe := obj::Account()
         _ := $joe.credit(100)

         ; ...
         abort $joe

         ; ...
         _ := $joe.credit(100)

The last assignment will trigger the evaluation of @credit(100) because $joe no longer exists. Notice that methods are called using the _ := action, because they are expressions. They have the same status as a function call. So they cannot appear directly as actions.

Calling a Method on an Object of Incorrect Type

Method calls check that the method is defined on the object given as an argument. If this not the case, then a function with the same name is looked at and applied on the arguments. If such function does not exist, an error is signaled.

Calling a Method Within a Method

All method calls in the object definition which refer to the current object instance can be written in an abbreviated infix form that omits the receiver:

          . method_name(a₁, a₂, ...)

instead of

          $THISOBJ . method_name(a₁, a₂, ...)

the special variable $THISOBJ refers to the object instance on which the method is called, see below). Obviously, the full syntax to call a method must be used if the receiver is not the current instance.

Accessing Object Fields in Methods

An object's field can be accessed directly through its $-name in the body of a method, as showed in the example by the body of the method credit. It is always possible to access to an object's field from “outside its methods” through the dot notation:

          obj . $field

where obj is an expression evaluating to the object reference (an exec).

Local variables in methods

Methods defined through @fun_def are specified using extended expressions. Such an expression may introduce local variables.

Routines are defined through a sequence of actions that may involve local variables. These variables are local to the routine instance (they cannot be accessed by others methods nor other routines). Actions in the routine body may have duration. Thus, several instances of the same routine may be active at the same moment.

Referring to the object: $THISOBJ

The variable $THISOBJ may appear in method definitions, where it refers to the object on which the method is applied, or in the clauses of an object definition, where it refers to the current instance.

This variable is special: it has a meaning only in the scope of a method or in the clauses of an object. It cannot be watched by a whenever. Assigning this variable leads to unpredictable result.

In a method body, a reference to an object field

         $THISOBJ . $field

can be abbreviated in

         $field

and a method called on the same object

         $THISOBJ . method (...)

can be abbreviated in

          . method_name(a , a , ...)

Specifying a Broadcast: @broadcast

Each broadcast clause defines a function with the same name. The syntax is similar to that of a function definition, except that it is introduced by the keyword @broadcast. Calling this function will execute the body of the function for each active instance of the object. The value returned is undef.

Here is an example where a broadcast is used to count the number of instances:

        @global $MyObj_count

        @obj_def MyObj()
        {
                @broadcast count() { $MyObj_count := $MyObj_count + 1 }
        }

        @fun_def countMyObj()
        {
            $MyObj_count := 0
            @count()
            return $MyObj_count
        }

        ; ...
        $number_alive_MyObj := @countMyObj()

A global variable $MyObj_count is used to add the number of instances. In the body of the broadcast, the fields of the object are accessible. A function @countMyObj is defined to reset the global variable, to broadcast the counting method and to return the result. This approach can be used to implement any function that do a global operation over all instances of an object.

Admittedly, using a global variable to share information between the various applications and the object instances can be troublesome. The broadcast mechanism will be extented to face this kind of problem in the next version of the language.

Specifying a Reaction: @whenever and @react

@whenever and @react clauses can be used to define the triggering of some actions or some expressions when some logical conditions occur. They are similar to (and implemented by) a whenever.

This construct makes it possible to define daemons that automatically respond to some events. There are two ways to do this. To launch actions, the syntax is:

          @whenever(expression) { actions }

and to launch an extended expression, the syntax is

          @react(expression) { extended_expression }

The second version is appropriate if the reaction consists in state update and instantaneous computations. The first form can be used to launch child processes and other durative actions. Note that because some actions are allowed in extended expressions, it is often possible to use one both forms interchangably. In either case, it is possible to use termination guards as in

          @react($x == $x) { $cpt := $cpt + 1 } until ($cpt > 3)

Patterns can be used to define complex condition in time.

Nota Bene: there is a daemon active for each object's instance.

Specifying an Abort Handler: @abort

The @abort clauses are gathered together and are launched when an object instance is killed. Object instances “live forever” (until a stop command) and they must explicitly be killed by an abort action.

Checking the Type of an Object: @is_obj and @is_obj_xxx

An instance of an object is implemented by a process, so it is of the exec type and the predicate @is_exec returns true on an object.

The predicate @is_obj can be used to distinguish between exec (object instances and process instances and more generally, instances of compound actions).

In addition, each time an object obj::xxx is defined using @obj_def, a predicate @is_obj_xxx is automatically defined. This predicate returns true if its argument is an object instance of obj::xxx.

Object Instantiation

An instance of an object is created using a syntax similar to that of a process call2:

          $metro1 := obj::Metro(2/3, "left_channel")

creates an object of type obj::Metro with a parameter $p that sets to 2/3 and a parameter $receiver that sets to "left_channel".

When an object is created, the @init clauses and the reaction clauses are performed in the order of their definition. Then, the object is alive and ready to interact. Interactions can happen through

  • method calls,

  • broadcasts,

  • direct assignments of the object fields (using dot notation)

  • changes in logical condition of reactions

  • and killing the object.

Concurrency Between Method Applications

Methods defined by @fun_def are evaluated instantaneously. In accordance with the synchrony hypothesis, their runs “cannot overlap” and correspond implicitly to atomic region. So there is no need to use semaphore, mutex or any other dedicated mechanism to implement mutual exclusion between method applications: mutual exclusion is given automatically.

Routine executions may persist in time. So, two routines called on the same object may run in parallel and may concurrently access the same fields. This may lead to consistency problems. However, sequences of atomic actions without delay are always executed instantaneously and in mutual exclusion with other such sequences: they are natural critical sections.

Object Expansion into Processes and Functions

The previous constructions are internally expanded in a process definition and in several function definitions, so there are no new evaluation mechanisms involved with the object constructions. However, they help in structuring the score3.

Keep in mind that an object, like any other process, can be synchronized. In particular, object inherits their synchronization from the synchronization strategy defined at their creation.



  1. However, you can apply them partially and use the partial application as a value (the value is of type (partially applied) function

  2. and is actually implemented by a process call 

  3. Objects are a new feature in Antescofo and are expected to evolve to integrate new mechanisms: we plan to integrate reactions linked to the reception of OSC messages, object fields shared by all instances, the unification of the broadcast mechanism with a map-reduce mechanism. In the long term, we want to develop inheritance mechanisms and the specification of more sophisticated concurrency constraints between routines.