Read Time: 4 mins
Eric Harris-Braun

Musings on Coding in Ceptr: Signaling

This is #2 in a series of dev posts, which just jump right into deep end of the techie pool.
At the core of Ceptr you will find agent-like receptors which send each-other messages. We've provided a simple yet sufficient instruction set for rich message programming, taking into account both synchronous and asynchronous programming needs.

The overall signaling model looks like this: Signal_Aspect_Receptor Membrane_Processing

  1. signals are sent on a carrier
  2. “through” an aspect
  3. and optionally keyed to a conversation or request
  4. are processed in the receptor's membrane
  5. by being matched against expectations associated with the aspect/carrier/conversation
  6. each of which trigger an action which either sends more signals or transforms the receptor's state
Requests: Some signals are requests, which, when received create a stateful condition of expecting a response to the requesting signal identifier. Additionally requests have end conditions, i.e. timeouts, response counts (there can be more than one), etc..

Listening: The LISTEN instruction allows you to add expectations (with their patterns and actions) on the flux (the receptor's membrane).

Conversations: Some signals are identified as being part of a conversation. Conversations are created with the CONVERSE scoping instruction. You can think of this instruction kind of like the "block" or "scope" construct that exists in most programming languages which creates a lexical scope for variables. CONVERSE creates a scope for sending and listening to signals. CONVERSE instructions can be nested, such that the system recognizes one conversation as a sub-part of another. The system also allows having multiple SAY/REQUEST instructions inside of a conversation to different destinations. This provides underlying support for various multi-party protocols.

Sync/Async: The signaling instructions have waiting and non-waiting versions. This allows you to program in both synchronous and asynchronous style. For example the REQUEST instruction has an optional action parameter. If provided, the REQUEST instruction reduces to the request signal's identifier, and the action process gets executed sometime later when the response arrives. Otherwise the tree with the REQUEST instruction goes into the waiting state and later, when the response message arrives, reduces directly to its body. The action parameter to the LISTEN instruction is similarly optional and indicates whether the tree with the LISTEN should wait or run asynchronously. Similarly for conversations, the CONVERSE instruction can be set to wait or not for the conversation to be completed.

Addressing: Receptor addressing in Ceptr has to take into account the fact that receptors exist inside of other receptors. In other words, the network topology isn't a two dimensional plane. Also, as a programmer, it helps to remember that the "place" where programs execute is in the membrane of the receptor. Thus you can visualize that any signals you generate are either internally bound, i.e. to the inside of the receptor, externally bound to a peer receptor inside the same parent, or bound to some receptor outside the parent. This means that each receptor gets to define its own address space to manage the coherence of messaging inside itself, as well as what messages go in and out. More on addressing in a future post...

Here's a summary of the instructions from the Ceptr instruction set that have to do with signalling:

Instruction Parameters Notes
SAY to_address, key_by_aspect, over_carrier, message Reduces to the signal's identifier
REQUEST Of_address, key_by_aspect, on_carrier, message, expect_response_on, until, *action If action provided then request reduces to the request's signal identifier. if no callback, then blocks and reduces to the response's message.
RESPOND respond_on_carrier, response_message Reduces to the response signal's identifier
LISTEN at_aspect, for_carrier, match_pattern, *action, *with_params, *until_end_conditions If action provided, reduces to REDUCTION_ERROR_SYMBOL(NULL_SYMBOL) and adds an expectation with the pattern to the flux. If no action, then builds a special END_CONDITION of COUNT=1 and blocks processing and then later reduces to the first signal that matches the pattern
CONVERSE scope, *end_conditions, *wait, *topic Defaults: end_conditions:UNLIMITED wait: FALSE
topic: none
Use end_conditions specify when the system will terminate the conversation. Use wait to specify whether the CONVERSE instruction should block waiting for the conversation to complete. If you use asynchronous REQUEST or LISTEN instructions in the scope the conversation and you don't set wait to TRUE you will want to use the THIS_SCOPE instruction to get the conversation identifier so you can call the COMPLETE instruction someplace later or the conversation will never get cleaned up.
COMPLETE with_something, *conversation_ident Cleans up a conversation and causes the CONVERSE instruction to reduce to value of with_something. If conversation_ident provided, completes that conversation, otherwise assumes the value of THIS_SCOPE. If the specified conversation doesn't exist (i.e. because it was cleaned up by its end conditions) COMPLETE will invoke the error handler. Note that it's possible that the CONVERSE instruction was already reduced, in which case the value of with_someting will get ignored. If the COMPLETE instruction is not inside the CONVERSE scope, it will be reduced to the value of conversation_ident
THIS_SCOPE Reduces to the identifier of the current conversation. Executing THIS_SCOPE not somewhere inside a CONVERSE will invoke the error handler.
SELF_ADDR Reduces to the receptor address of the current receptor.