Escher A language for connecting technologies using pure metaphors

Implementing reflexes

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.

Reflexes and the 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.

Receiver type

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.

The spark

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 {
	…
}

Eye to the outside

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.

Materialization matter

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)
	}

Auxiliary input

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.

Return residue

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.

Receiver methods

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.

Fixed valve names

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.

Varying valve names

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.

Linking user reflexes into the runtime

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.

A one-way door example

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{}{}
}