logo
The Falcon Programming Language
A fast, easy and powerful programming language
Location: Home page >> Falcon wiki
User ID:
Password:
 

Falcon Wiki - Survival:Message oriented programming


Message Oriented Programming

Message oriented programming (MOP) consists in writing program sections generating and replying to messages (happening now, in the future or even happened in the past) instead of writing direct calls.

Falcon MOP is constituted by three distinct inter-operating entities:

  • subscriptions: requests to be notified about events.
  • broadcasts: generation of temporary messages.
  • assertions: persistent messages that stays in the environment until retracted.

A message is formed by a name (also called event) and zero or more parameters. The event is always and exclusively a string, while any Falcon item can be used as parameters.

As a simple example of MOP, let's rewrite a MOP-oriented printl:

subscribe( "printl", {tbp => >tbp} )
broadcast( "printl", "Hello world!" )

The subscribe call informs the system that we want to reply to printl events through the given handler, in our case, the given code block that just prints one parameter. The broadcast call sends the parameters (in this case, just "Hello world!") to all the subscribed handlers, if there is any.

Assertions

Falcon messaging model allows to post a single item temporarily or permanently associated with a message.

For example, suppose that some module can be started either before or after some "goodStuff" get readied. We want to complete our work only after we can put our hands on the "goodStuff". So,

class MyStuff
   // private data...
   ready = false

   init
      subscribe( "goodStuff", self.configure )
   end

   // more stuff...
   
   function configure( stuff )
      // good, we have the good stuff
      //... use the stuff to do things...
      self.ready = true
   end
end

In another part of the program, the assertion can be posted like this:

assert( "goodStuff", "Some stuff to be sent around" )

This causes all the already created instances of MyStuff to be configured at once, and allows new instances created from now on to be immediately configured.

Listeners can unsubscribe from listening messages and assertions through the unsubscribe function, passing the event to which they wish to unsubscribe and themselves.

For example, as configuration is one-time action, the above MyStuff class may wish to unsubscribe once received the message, so the configure method can be rewritten as:

   function configure( stuff )
      // ... rest as before
      unsubscribe( "goodStuff", self.configure ) 
   end

It is possible to issue a broadcast with the same name of an existing assertion, so subscriptions to events will respond both to assertions and broadcast on that event. Asserting over a previously existing assertion replaces the previous one with the new data, and also notify the change by re-broadcasting the new value.

To remove an existing assertion use the retract function:

// no more good stuff this days
retract( "goodStuff" )

Retracting a non-existing assertion will raise an error.

Finally, it is possible to query for the current value of an assertion:

assert( "goodStuff", "Really good stuff" )
> getAssert( "goodStuff" )

Normally, getAssert function raises an error if there isn't any assertion active on the required event, but it's also possible to provide a default value that is returned in case the assertion isn't found, as in the following example:

> getAssert( "non-existing", "Ops, we didn't found an assert" )

Broadcast Control

Broadcast is performed synchronously. The caller of broadcast waits that the subscribers reply to the broadcast in turn, and returns the value that was returned by the last handler.

The order by which handlers are called is the same order in which they have subscribed. To prevent other handlers to get in control of the message, the subscriber must call the function consume, which will grant that after it returns, the broadcast will be interrupted and the broadcast function will return its same return value. The following example shows how an appointed subscriber can reply to a message returning a value to the broadcast caller.

// create a couple of receivers
function f1( target, value )
   if target == "by100"
      consume()
      return value * 100
   end
end

function f2( target, value )
   if target == "by500"
      consume()
      return value * 500
   end
end

// subscribe the two receivers
subscribe( "multiply", f1 )
subscribe( "multiply", f2 )

// multiplying a value depending on the target:
> "Mult 2 by 100: ", broadcast( "multiply", "by100", 2 )
> "Mult 2 by 500: ", broadcast( "multiply", "by500", 2 )

// and a non-existing target...
> "Mult 2 by 1000: ", broadcast( "multiply", "by1000", 2 )

A late subscriber can be put on top of the subscribers list by passing an extra true value as the last parameter of subscribe. In this way, a message filter coming after other subscribers can prevent the passage of the message to original subscribers by consuming it:

function f1(): > "I am f1"
function f2(): > "I am f2"
function f3(): > "I am f3"
function f4(): > "I am f4"

// subscribe regularly f1 and f2
subscribe( "printme", f1 )
subscribe( "printme", f2 )

// but give more priority to f3 and f4
subscribe( "printme", f3, true )
subscribe( "printme", f4, true )

broadcast( "printme" )

As seen, the call order becomes {f4, f3, f1, f2}, as the topmost item is f4, inserted with priority on top of f3, they both inserted with priority with respect to f1 and f2.

It is possible to broadcast a message in without waiting for its result creating a coroutine ad hoc for this. We'll explain coroutine in a later chapter, but it's worth to see that, if one doesn't need the broadcast to be in sync with the caller, it can write it as:

launch broadcast( "anEvent", ... )

Similarly, it is possible to perform a broadcast from a different thread, but threading is argument for another document.

Cooperative broadcast

At times, it's not just useful to "steal" the broadcast signal from the following handlers. It may be also useful to cooperate to form a common result, or to perform different steps of a common work.

Broadcast parameters can be any Falcon item, including arrays, dictionaries and objects. Each participant in the broadcast may alter or manipulate the incoming item(s), as in the following example, where a first subscription step allows participants to ask for a subsequent call.

class Subber( id )
   id = id

   init
      subscribe( "process", self.subMe )
   end

   function subMe( requests )
      // will I be taking the second step?
      if random(10) < 5: requests += .[ self.callMe ]
   end

   function callMe()
      > @"Subber $self.id was called back!"
   end
end

// all the subbers
subbers = []
for i in [0:10]: subbers += Subber(i+1)

// action they want us to do 
requests = []
broadcast( "process", requests )

// do them
for req in requests: req()

The broadcast of the above example gives all the listener the chance to post a request in the vector that is being formed during the process. When the broadcast is complete, the main program executes each posted request.

This allow also for "auction" based broadcast, where the subscribers find an agreement on who should actually process the message as described as the following example shows:

class BidToken
   value = 0
   callback = {=> >"No winner" }
end

class Player( id )
   id = id

   init
      subscribe( "play", self.subMe )
   end

   function subMe( tk )
      // tk is a bid token.
      bet = random(10)
      if bet > tk.value
         tk.value, tk.callback = bet, self.callMe
      end
   end

   function callMe()
      > @"Player $self.id won the auction!"
   end
end

// all the players
players = []
for i in [0:10]: players += Player(i+1)

// action they want us to do 
tok = BidToken()
broadcast( "play", tok )
// elect the winner
tok.callback()

Automatic marshaling

Objects, instances and blessed dictionaries can be used directly to receive broadcasts, even if they are not callable items. By subscribing a message with an instance, the subscriber indicates that it wants that message marshaled to a function named "on_" + the event name. For example, if an object is suscribed to an event named "bcast", in case "bcast" is broadcast, its "on_bcast" method will be called back.

object Processor
   init
      subscribe( "bcast", self )
      subscribe( "evt", self )
   end

   function on_bcast(): > "Received a bcast"
   function on_evt(): > "Received an evt"
end

broadcast( "bcast" )
broadcast( "evt" )

In case the subscribed item doesn't provide the needed callbacks, a MessageError is raised. The check is performed at broadcast time because prototype oop constructs may change their structure in the meanwhile, and class instances may change the type of existing properties, making them non callable.

Message Slots

Falcon manages subscriptions, broadcasts and assertions through an object called Slot. This object is reflected and accessible from scripts through the VMSlot class.

A VMSlot can be accessed through two means: using the getSlot function or creating a VMSlot instance. In the first case, if the desired slot doesn't exist (that is, if there aren't any subscriptions or assertions active for that slot), an error is raised; in the second case, the slot is created anyhow, and eventually connected to the Slot data if it's not empty.

Operating on VMSlot methods is totally equivalent to using the homologous functions; the only difference is that the event parameter is missing, as it's included in the VMSlot object, and any operation on VMSlot is faster as the VM doesn't need to search its internal slot database for the required event.

For example:

slot = VMSlot( "event" )

subscribe( "event", function(); > "Received an event"; end )
slot.broadcast()  // == broadcast( "event" )

or even:

subscribe( "event", function(); > "Received an event"; end )
slot = VMSlot( "event" )  // connects to "event" subscriptions
slot.broadcast() 

It's possible to create multiple instances of the same VMSlot, even in different modules and in different threads; they all refer to the same internal Slot data. So, in case you want to use a VMSlot for faster operation in different unrelated parts of your program, you can simply create a local instance.

Iterating on VMSlots

An interesting characteristic of the VMSlot abstraction is that it is possible to iterate over them in for/in loops. Each loop receives a subscriber in the same order respected by broadcast. Using continue dropping to delete an item from the collection has the same effect as unsubscribing the handler.

For example:

// some subscriber
function f1(): > "I am f1"
function f2(): > "I am f2"
function f3(): > "I am f3"

// subscribe all of them
slot = VMSlot( "bcast" )
dolist( slot.subscribe, [f1,f2,f3] )

// Let's say we don't like f2
for subscriber in slot
    >> subscriber
    if subscriber == f2
       > " (we don't like it)"
       continue dropping
    end
    > " (ok)"
end

// broadcast
>
> "Broadcasting now."
slot.broadcast()

The broadcast will activate only f1 and f3 as f2 has been effectively unsubscribed.

**Warning**: Better not to use this when the slots can be broadcast from other threads without a proper protection to avoid concurrency on the broadcast list.

The VMSlot class provides also first and last methods that return an iterator that can be used to scan and modify the subscription list.

Anonymous Slots

Since version 0.9.6.4

Slots can be created without a name. To register and broadcast on this anonymous slots, it is necessary to have a reference to them, as they can't be individuated by name.

sl = VMSlot()
sl.subscribe( { data => printl( "Called with ", data) } )
sl.broadcast( "Some data" )

Events

Since version 0.9.6.4

Events are messages specific to a single VMSlot, which have a name overriding that of the owning slot. They cannot be invoked from the broadcast function, as they are specific of the VMSlot on which they are related to.

To generate an event instead of a broadcast message, the caller must use the VMSlot.send method. The first mandatory parameter of this method is the event name, which must be a string; the other parameters will be posted to the listeners as in the case of VMSlot.broadcast.

On a subscriber point of view, the main difference is that automatic marshaling is directed to the name of the generated event rather to the name of the slot. So, for a receiver, it is possible to subscribe to a slot (named or anonymous), and then be automatically subscribed to all the events that are generated on that slot. If the receiver is a function, it will receive the event name as the first parameter. If the receiver is an object, a class instance or a blessed dictionary, the event will be received by the method called "on_<event>", where <event> is the event name specified in VMSlot.send. Also, in this latter case, if the object exposes a method called __on_event, that method will be called in case there isn't any adequate "on_<event>" method to be called back. The catch-all __on_event method will receive the event name as the first parameter, and all the other parameters sent by the event.

See the following code:

   object Receiver
      _count = 0

      function display(): > "Count is now ", self._count
      function on_add(): self._count++
      function on_sub(): self._count--
      function __on_event( evname ): > "Received an unknown event: ", evname
   end

   s = VMSlot()  // creates an anonymous slot

   s.subscribe( Receiver )

   s.send( "add" ) // Instead of sending a broadcast ...
   s.send( "add" ) // ... generate some events via send()

   s.send( "A strange event", "some param" )  // will complain
   Receiver.display()   // show the count

Callbacks subscribed to the slot via the VMSlot.subscribe method will be excited no matter what specific event is generated through VMSlot.send, but it is possible to register callbacks to respond only to particular events via the VMSlot.register method.

See the following example:


slot = VMSlot()  // creates an anonymous slot
slot.register( "first",
   { param => printl( "First called with ", param ) } )

slot.register( "second",
   { param => printl( "Second called with ", param ) } )

// send "first" and "second" events
slot.send( "first", "A parameter" )
slot.send( "second", "Another parameter" )

// this will actually do nothing
slot.broadcast( "A third parameter" )

In the above example, as no callback is subscribed to the slot via VMSlot.subscribe, a generic broadcast will have no effect. Callbacks registered to events via the VMSlot.register will be called back only if that specific event is generated via VMSlot.send.

Events and Sub-slots

An interesting thing about registering to events is that a slot keeps tracks of callbacks and items registered to a specific event via a named slot, which is considered a child of the parent slot. That child slot can be accessed via the VMSlot.getEvent method, and it can be manipulated as any other slot. Calling VMSlot.broadcast() on the child slot has the same effect of calling the VMSlot.send method on the parent slot; so:

parent = VMSlot() // an anonymous slot
parent.register( "evt", {=>"Evt has been generated!"} )
parent.send( "evt" )

// Get the slot representing the "evt" event:
child = parent.getEvent( "evt" )
child.broadcast()  // same effect as parent.send("evt")

The child slot is a normal VMSlot in every aspect. For example, to know who is currently subscribed to an event, it's possible to call the VMSlot.getEvent method and inspect the returned slot.

For example, continuing the above code...

   //...
   // display each subscribed item
   for elem in child: > elem.toString()

Any change in the child slot will cause the event registration to change. To unregister an event, it is possible to get the child slot and remove the registered event receiver.

It's good practice to cache frequently used events and call VMSlot.broadcast on them, instead of using VMSlot.send on the parent slot.

This structure can be freely replicated at any level. In the above example, child may be subject of send() and register() methods, and its own events can be retrieved trough its VMSlot.getEvent method.


Navigation
Go To Page...

Loading

Elapsed time: 0.040 secs. (VM time 0.032 secs.)