A key motivation for the design of Escher is the idea that software programs should be assembled as the interconnection of independently-executing computational devices of special-purpose logic. In other words, computer programs—small or large—should be no different in their essential structure than cloud applications, which are no more and no less than an interconnection of independently running special-purpose services.
We call these “computational devices” reflexes. Reflexes can be implemented in the language underlying Escher (the Go language) or they can be composed out of other reflexes, using circuit programs from within Escher. Here we describe how to implement relfexes in Go and link them into the Escher runtime.
A reflex is an independent computing device which can communicate with other reflexes through a set of named valves.
The creation and execution of a reflex is called materialization. The Escher runtime materializes reflexes as parts of larger circuits of interconnected reflexes. When a reflex is materialized, its set of connected valves is already determined by the higher-level runtime logic, which in turn is guided by circuit programs, described in the next section.
Every reflex is embodied by a user-defined Go receiver type.
type Receiver struct { … }
The receiver type must be a Go struct
or pointer to struct
.
When a reflex is materialized, the Escher runtime creates a new instance of the underlying
Go receiver type and invokes a designated initialization method, called Spark
.
All receivers must implement that method.
func (r *Receiver) Spark(eye *Eye, matter Circuit, aux ...interface{}) Value { … }
The first argument eye
is an object with a singleton public method:
func (eye *Eye) Show(valve Name, value interface{})
You can use this method to send values to any valve connected to this reflex,
specified by its name. The method eye.Show
should not be called
from the body of Spark
directly, but it can be invoked from
a go-routine spawned from the body of Spark
.
It is usually not necessary to save the eye
in the receiver's fields,
because it is passed to all public methods of the receiver (described below) that
the runtime calls.
The argument matter
holds the entire runtime “language stack” that lead
to the materialization of this reflex. This object holds the same debugging information that
is printed out by the Escher tool when an Escher program panics.
From a programmatic standpoint, only one of the gates of circuit matter
is of interest to reflex programmers. The gate called View
lists
the names of all valves connected to this reflex by the parent system which is
materializing this reflex. The View
gate has
a circuit value, whose gate names correspond to the names of the
valves connected to the reflex being materialized.
For instance, the names of the connected valves can be printed with this code:
view := matter.CircuitAt("View") for _, valve := range view.SortedNames() { fmt.Printf("valve name = %v\n", vavle) }
The last argument aux
contains user-supplied auxiliary
information that can inform the Spark
method to specialize
this reflex one way or another. The auxiliary information is specified
by the user when linking the reflex to the runtime, which is explained
furhter below.
The Spark
method can return a value called the
residue (of materializing this reflex). The residue value
can be int
, float64
, complex128
,
string
, Circuit
or Materializer
.
The latter is a Go type that can materialize reflexes (it is essentially a factory
object for reflexes), described in the linking section below.
The residue will be made available through the Escher programming environment for further manipulations.
There are two kinds of public receiver methods that the runtime considers (by reflecting on the receiver's Go type) when materializing a reflex implementation.
The first kind are receiver methods named
CognizeVALVE
, where VALVE
can be any string (including the empty string), that have the following
signature:
func (r *Receiver) CognizeVALVE(eye *be.Eye, value interface{}) { … }
If such a method is present in Receiver
, it informs the
runtime that this reflex type requires the valve named VALVE
to be connected (when the reflex is materialized as part of a circuit of reflexes).
Furthermore, every event sent to this valve (of this reflex instance)
will result in an invokation of the method CognizeVALVE
, wherein the event value
is held by the argument value
. The eye
object, supplied
for convenience, can be used to send out events to any of the reflex's connected
valves.
We say that that the method CognizeVALVE
captures the event.
The second kind are receiver methods with this exact signature:
func (r *Receiver) OverCognize(eye *be.Eye, valve Name, value interface{}) { … }
If such a method is present, the runtime is informed that the reflex
accepts any number and naming of connected valves. The method
OverCognize
will be invoked whenever an event is
received that is not captured by a fixed-name valve method.
The name of the valve that the event is received on will be held
by the argument name
in this case.
Before a new reflex receiver type can be used by the runtime for materialization,
one must create a Materializer
object, which acts as a factory
for reflexes of a given type.
Creating the Materializer
is accomplished using the function
NewMaterializer
in package be
:
func NewMaterializer(receiver Material, aux ...interface{}) Materializer
When the Escher runtime (implemented in escher/main.go
) starts,
it creates a global index circuit (i.e. a namespace) of all reflex materializers that
will be available from the Escher circuit programming environment.
To add a materializer for a new reflex type to the Escher index, one uses
the method Register
in package faculty
:
func Register(v Materializer, addr ...Name)
The first argument is the materializer for the reflex, obtained from NewMaterializer
,
and the second argument is the address within the index where the materializer will be placed.
Typically the user will implement a package with multiple topically-related reflex receivers,
and will register their respective materializers with the runtime as a side-effect of importing the
package, using an init
function:
func init() { faculty.Register(be.NewMaterializer(&Receiver{}), "example", "ReflexName") }
To include user-defined reflexes in the Escher executable, edit escher/main.go
to import the newly created package.
The following code demonstrates implementing and linking a new
reflex. The purpose of this reflex is to act as a “one way door”.
It expects exactly three connected valves From
, To
and Door
.
Values received on valve To
are ignored. When a value is received
on valve From
, it is not passed on to valve To
until
an arbitrary value is first sent to valve Door
.
In other words, the reflex passes values from From
to To
,
wherein each passing value is blocked until its transmission is allowed by a “strobe”
value sent to Door
.
package example import ( "github.com/gocircuit/escher/be" "github.com/gocircuit/escher/faculty" . "github.com/gocircuit/escher/circuit" ) func init() { faculty.Register(be.NewMaterializer(&Door{}), "example", "OneWayDoor") } type Door struct { flow chan struct{} } func (r *Door) Spark(*be.Eye, Circuit, ...interface{}) Value { r.flow = make(chan struct{}) return nil } func (r *Door) CognizeFrom(eye *be.Eye, value interface{}) { <-r.flow eye.Show("To", value) } func (r *Door) CognizeTo(eye *be.Eye, value interface{}) {} func (r *Door) CognizeDoor(eye *be.Eye, value interface{}) { r.flow <- struct{}{} }