1 RRLL: Actor Basic
Racket Rogue-Like Library: Actor Basic
Single-value flow-oriented language differentiating between flow steps and value expressions.
1.1 Introduction
Actor Basic is a S-Expression language. The basic element of the Actor Basic language is a S-Expression list with abasic-lambda as its first element and it forms an Actor Basic program. The Racket representation of such program is then a procedure which accepts a single argument (the actor), passes it through the steps of the program which may modify it and eventually returns the final actor as updated by all the steps performed.
The Actor Basic Actor must implement to gen:abasic-actor interface.
To create modules with Actor Basic programs, use the s-exp meta-language:
#lang s-exp rrll/abasic ...
For the structure of Actor Basic programs, see the rrll/abasic/expander.
When used as a standalone language, the define is an alias for define/provide-abasic from rrll/abasic/define.
1.2 Actor Basic Message
| (require rrll/abasic/message) | package: rrll-abasic |
This module provides a simple struct for sending messages to actors with optional data and queue for sending replies back.
procedure
code : symbol? queue : (or/c #f tiqueue?) = #f data : any/c = #f
1.3 Actor Basic Generics
| (require rrll/abasic/generic) | package: rrll-abasic |
This module provides a generic interface that must be implemented by any struct used in actor basic procedures.
value
gen:abasic-actor : any/c
procedure
(abasic-actor-send! actor v) → void?
actor : abasic-actor? v : any/c Must send the message to given actor.
procedure
(abasic-actor-recv! actor) → any/c
actor : abasic-actor? Must receive a message for given actor.
1.4 Actor Basic Steps
| (require rrll/abasic/steps) | package: rrll-abasic |
The steps represent actions that the actor can perform. The step specification is always a procedure that may accept optional arguments (if defined) and the result is a procedure that is applied to current actor state and returns a new actor state.
value
= (-> any/c ... (-> abasic-actor? abasic-actor?))
Performs nothing if no such channel is bound.
The actor state is returned unchanged.
1.5 Actor Basic Expander
| (require rrll/abasic/expander) | package: rrll-abasic |
The expander compiles Actor Basic down to simple Racket forms that can be efficiently compiled.
For compatibility reasons the expander re-exports bindings of certain forms from racket/base and exports bindings of the new forms it introduces. It is an error to use any of these in improper context.
syntax
syntax
syntax
syntax
syntax
syntax
syntax
syntax
syntax
syntax
syntax
syntax
syntax
syntax
(abasic-lambda maybe-formal-arg steps ...)
Each Actor Basic program has the following structure:
| ‹program› | ::= | ( abasic-lambda ( ) ‹steps› ) |
|
| | | ( abasic-lambda ‹racket-expr› ‹steps› ) |
| ‹steps› | ::= | |
|
| | | ‹step› ‹steps› |
|
| | | ‹form› ‹steps› |
| ‹racket-expr› | ::= | any Racket expression that evaluates to single value |
When the second form of the abasic-lambda is used, the value given as formal parameter is actually substituted for default value when the program is run with no input value. It defaults to #f.
Emacs note: For reasonable indentation of the abasic-lambda expressions, put the following into your ~/.emacs file.
(put 'abasic-lambda 'racket-indent-function 1)
Assumes racket-(xp-)mode.
The steps may be primitive steps or special forms:
| ‹step› | ::= | ‹s-id› |
|
| | | ( ‹s-id› ‹args› ) |
| ‹form› | ::= | ‹loop› |
|
| | | ‹case› |
|
| | | ‹cond› |
|
| | | ‹recv› |
|
| | | ‹recv-case› |
|
| | | ‹recv-case*› |
|
| | | ‹with-recv› |
|
| | | ‹with-recv-case› |
| ‹s-id› | ::= | bound identifier to abasic-step? |
| ‹args› | ::= | |
|
| | | ‹expression› ‹args› |
|
| | | ‹#:keyword› ‹expression› ‹args› |
| ‹#:keyword› | ::= | any keyword? |
The loop form is used to expres tail-recursion loops like with named let.
| ‹loop› | ::= | ( loop ‹steps› ) |
The case form compares given value - which may be obtained by arbitrary Actor Basic expression to either single constant tokens (primitive Racket values) or a list of such using eq? and the first one that matches chooses different branch of steps for continuing the evaluation of the program.
If none matches, there may be a default else branch as the last case. If none matches and there is no default branch, the result of evaluating the case is equivalent to no-operation and the current actor is passed through as-is. There can be no further case branches after the default else branch.
| ‹case› | ::= | ( case ‹expression› ‹cases› ) |
| ‹cases› | ::= | |
|
| | | ( ‹single-case› ) ‹cases› |
| ‹single-case› | ::= | ( ‹value› ‹steps› ) |
|
| | | ( ( ‹values› ) ‹steps› ) |
|
| | | ( else ‹steps› ) |
| ‹value› | ::= | |
| ‹values› | ::= | |
|
| | | ‹value› ‹values› |
The cond form checks all the conditions in sequential order until it one evaluates to non-#f value and that branch of steps is chosen for further evaluation of the program. The conditions are Actor Basic expressions.
If none matches and there is a else branch, it is chosen. Without else branch the whole form is essentially no-operation and passes the current actor through as-is.
| ‹cond› | ::= | ( cond ‹conds› ) |
| ‹conds› | ::= | |
|
| | | ‹single-cond› |
|
| | | ‹conds› |
| ‹single-cond› | ::= | ( ‹expression› ‹steps› ) |
|
| | | ( else ‹steps› ) |
Receiving messages by the Actor Basic program is possible if the given actor implements the gen:abasic-actor interface through the recv form or one of its derived forms. The recv form can bind:
only the received message code, or
the received message code and additional data, or
the code, data and tiqueue? for sending replies
| ‹recv› | ::= | ( recv ‹code› ‹steps› ) |
|
| | | ( recv ( ‹code› ) ‹steps› ) |
|
| | | ( recv ( ‹code› ‹data› ) ‹steps› ) |
|
| | | ( recv ( ‹code› ‹data› ‹queue› ) ‹steps› ) |
| ‹code› | ::= | identifier, to which the message code symbol? will be bound |
| ‹data› | ::= | identifier to which the additional data will be bound |
| ‹queue› | ::= | identifier to which the queue for sending replies will be bound |
Combination of recv and case forms - a recv-case form - is provided to allow simple message dispatching when no further processing is needed. The value which is tested agains the various cases is the message code received by recv.
| ‹recv-case› | ::= | ( recv-case ‹cases› ) |
The recv-case form is internally expanded as follows:
(recv-case (1 (debug "received 1")) (else (debug "received something else")))
The previous form becomes:
(recv msg (case msg (1 (debug "received 1")) (else (debug "received something else"))))
If easy message dispatching is desired in addition to binding the message contents, the recv-case* form can be used:
| ‹recv-case*› | ::= | ( recv-case* ‹code› ‹cases› ) |
|
| | | ( recv-case* ( ‹code› ) ‹cases› ) |
|
| | | ( recv-case* ( ‹code› ‹data› ) ‹cases› ) |
|
| | | ( recv-case* ( ‹code› ‹data› ‹queue› ) ‹cases› ) |
The expansion of recv-case* is similar to racket-case.
| ‹with-recv› | ::= | ( with-recv ( ( ‹cc› ) ‹steps› ) ‹steps› ) |
| ‹cc› | ::= | identifier, to which the message code symbol? will be bound |
| ‹maybe-recv-args› | ::= | ‹code› |
|
| | | ‹code data› |
|
| | | ‹code data sender› |
The with-recv form installs an asynchronous message handler in which the cc is bound to the continuation that - when evaluated as a step of the flow - resumes flow evaluation after the message interrupt was received. If it is not used and the handler steps reach the end of the form, evaluation of the flow continues right after the with-recv form.
As the with-recv form does not actually recv the message, a convenience form is provided:
| ‹with-recv-case› | ::= | ( with-recv-case ( ( ‹cc› ‹maybe-recv-args› ) ‹cases› ) ‹steps› ) |
The helper form is roughly expanded as follows.
(with-recv-case ((cc msg data sender) cases ...) steps ...)
After expansion it becomes:
(with-recv ((cc) (recv-case* (msg data sender) cases ...)) steps ...)
Unlike with-recv the with-recv-case immediately receives the message using recv and therefore although the with-recv handler behaves like level-triggered interrupt, the interrupt condition is immediately handled and the handling will not happen again for the same message.
Actor Basic expressions can be used as step arguments, predicates in cond form, value in case form and arguments of arbitrary procedures within the expressions themselves.
| ‹expression› | ::= | ( quote ‹racket-expr› ) |
|
| | | ( and ‹expressions› ) |
|
| | | ( or ‹expressions› ) |
|
| | | ( ‹p-id› ‹args› ) |
|
| | | ‹p1-id› |
|
| | | ‹value› |
| ‹expressions› | ::= | |
|
| | | ‹expression› ‹expressions› |
| ‹p-id› | ::= | bound identifier to a procedure |
| ‹p1-id› | ::= | bound identifier to a procedure accepting one argument |
The quote expression treats its contents like Racket’s quote does.
The and expression evaluates the expressions it contains one by one until it reaches either the end of the list or an expression that evaluates to #f either of which it returns. Without any expressions, sole (and) returns #t.
The or expression evaluates the expressions it contains one by one until it reaches either the end of the list or an expression that evaluates to non-#f which it returns. In the former case it returns #f. Without any expressions, sole (or) returns #f.
When the first argument of a list expression (multiple expressions enclosed in parentheses) is bound to a procedure, it is applied and before the application all its argument values are treated as Actor Basic expressions.
In the procedure application, it is not possible to evaluate complex expression in the first position of the list (the procedure to be applied must be an identifier, not an expression).
If a single identifier is to be evaluated and it is bound to a procedure that accepts exactly one argument, it is applied to the current actor and the result of the application is the result of this identifier evaluation. The current actor is not altered in such case - it can only be updated by steps through which the data structure flows.
Otherwise a single identifier or simple value is treated as-is and it is the result of such primitive expression.
1.6 Actor Basic Flow Graphs
| (require rrll/abasic/graph) | package: rrll-abasic |
This module provides alternative compilation of abasic-lambda to dot language - the graphviz source file.
syntax
(abasic-graph-print () step ...)
(abasic-graph-print default step ...)
The initial value is added as the starting node of the flow.
All the steps of the program are then added as follows:
Sole identifiers not linking to loop binding are added as single node connecting all open ends of the current context to this node and designates this node as the only and current open end in current context.
Special loop form creates a new graphviz cluster subgraph and creates a new node for the loop label within this cluster. It connects all open ends in the current context to this newly created node. Then it pushes a new context with this node as the open context onto the stack and processes all steps within the loop form recursively. After finishing it collects all open ends from the newly created context removes that context and adds the collected open ends to the original current context on stack.
The when form creates a new node and links all open ends to this node. Then it creates a new context on stack and processes all its steps recursively within this context. Then it collects all open ends from the newly created context which it removes from the stack and adds the form node to these open ends as well.
The cond form creates a new node and links all open ends to this node like when does. It also creates a new context similarly but at the end it checks whether there was a else subform and if it was NOT, it adds the created node to the list of open nodes in the surrounding context in addition to what it colleceted from within the cond form. Links to all steps of all subforms will be labelled based on their respective condition expression.
The case works like a cond, but the node created also contains the expression to be compared to the cases. The links to subforms are labeled with the tokens to match in each of them.
The recv form creates a new graphviz cluster subgraph and a new node for the form inside it. It connects all open ends to this node and then it creates new context on stack with only this node as the current open end.
1.7 Actor Basic Define
| (require rrll/abasic/define) | package: rrll-abasic |
This module sprinkles some syntactic sugar on top of the expander to allow simple usage when Actor Basic is used as standalone language.
syntax
(define-abasic (name) step ...)
(define-abasic (name value) step ...) (define-abasic ((name arg ...)) step ...) (define-abasic ((name arg ...) value) step ...)
The second form does the same while specifying alternative default value to the program argument.
The third - curried - form creates a procedure that returns the compiled program when evaluated.
The fourth - also curried - form creates a procedure accepting some arguments that creates a procedure that returns the compiled program when evaluated with actual argument values.
syntax
(define/provide-abasic (name) step ...)
(define/provide-abasic (name value) step ...) (define/provide-abasic ((name arg ...)) step ...) (define/provide-abasic ((name arg ...) value) step ...)
1.8 Scribbling Steps
| (require rrll/abasic/scribble) | package: rrll-abasic |
This module can be used to easily write scribblings of Actor Basic Steps.