Containers
In this chapter you will become acquainted with the container unit
to encapsulate a circuit as a new unit.
How to create a container
A container is created like any other NST unit: drag the container icon
from the folder window into the circuit window and specify the desired
interface (i.e., the input and output connectors that the container shall
have).
The left half of the dialog window allows you to specify up to 30 input
connectors. Each line in this half which starts with the leftmost button
set to "inp" specifies the first pin (or scalar pin group, cf. below)
of a new connector. If the leftmost button is instead set to "-",
the line specifies an additional pin (or scalar pin group) for the
current connector.
The pin type is selected with the next button, its dimension with the
next input field. If the type is float and the dimension>1, then the entry
will specify an entire group of consecutive scalar float pins. If
the dimension is <0, this is treated as the specification of a float[]
vector pin whose dimension is the absolute value of the entered (negative)
dimension value.
In all other cases, the entry will specify a single (vector) pin of
the given dimension (with one of the types float[], int[] or char[]
for
float,
int or byte vector). If the vector dimension is 0 in one
of these three cases, the pin will be a dynamic vector pin.
Finally, "string" and "special" select the string pin
type (with the dimension specifying an initial minimal size, which, however,
will be dynamically expanded as required [and always be rounded to a multiple
of an internally used block size]) and the user-defined pin type (this
will ignore the dimension).
In an analogous fashion, the right side allows to specify the output
connectors. After "OK" a container unit according to the given specification
is made. A second parameter dialog allows you to choose a label for the
created unit. Create some example containers with different types of connectors!
How to modify a container
To modify a container, select its 'm' field. You will get an almost-copy
of the container's create dialog box. However, at the bottom there are
some additional buttons to allow you to change the container's label ("relabel")
or to insert new or delete old connector or pin entries without having
to re-type the part below the insertion/deletion point. To delete a particular
pin or connector, select its dimension entry with the mouse pointer (use
left mouse button), the select button "DelPin" or "DelFld".
To insert a new pin or connector, first select the dimension entry directly
below the desired insertion point, then select
"InsPin" or "InsFld"
to create a gap in which you may enter your specification. When a deleted
or to-be-modified pin is connected to wires, the change may be refused
(in this case, you first have to disconnect the pin before you can make
the change). Note also that there is no CANCEL option in this case: you
can only accept your changes or undo them manually!
How to use a container
The purpose of a container is to "encapsulate" a visual program so that
it appears as a single object (with input and output connectors) from the
"outside". Therefore, each container has a 'o' symbol which you
can click at to "open" the container. The circuit window will then
display the interior of the container, with any connectors of the container
appearing at the left (for inputs) or the right (for outputs) side. Here
is an example#1. (it draws
the famous "Feigenbaum graph", which is the attractor of the logistic
equation x(t+1)=a*x(t)*(1-x(t)) as a function of the "gain parameter"
a).
To get back to the outer level, press the right mouse button. When you
are inside the container, any newly created NST units will become
"subunits"
of the container, i.e., will be located inside. Inputs and outputs of such
subunits can be made accessible from the outer level by connecting them
to suitable input or output connectors of the enclosing container (following
the same type matching rules as explained previously with the only change
that now inputs can only be connected with inputs, and outputs only with
outputs). Executing a container has the effect of executing all of its
subunits (the container itself has no operations -- except for possible
type conversions at its connectors -- associated with it). This is the
basis for structuring large visual programs in a hierarchical manner.
How to use the return_unit
The return_unit provides a
construct similar to a (conditional) return statement for a subprogram:
when a return_unit is inside a container AND the return_unit's input is
non-zero when it is executed, execution will jump back to the next outer
level and continues with the first unit after the container that encloses
the return_unit (if the container is the last unit in another container,
execution jumps again to the next outer level, and so on, until a next
unit is found, or the current "Step" is completed).
How to call named
subunits of a container
You can call (e.g., with the use_method
unit) named subunits of a container "from outside", if the enclosing container
has a name, too. In this case, the call requires that the two names are
concatenated (with a colon ':' as separator) into a "path name",
such as "mycontainer:mysubunit". This scheme generalizes to deeper
calls into multiply nested named containers. Here is an example#2,
in which a named container "foo" has a named subunit "foo" (which
is an output_window instance),
and a named container "bar", containing another named subunit "deep"
(again an output_window instance) one nesting level deeper. The other three
units at the top level are two use_method
units and a use_named unit that
call the subunits "foo:bar" and "foo:bar:box" by their path
names (click at their 'x'-field to see the effect).
Note that also the use_method unit automatically "mirrors" the interface
of the referenced subunit. In the present case, we do not wish to change
any of the current inputs of the referenced units, therefore, the use_method
units are instantiated with transport of input data restricted to "connected
inputs only" (the inputs at our use_method units are unconnected, no
data will be transported, as desired). The use_named
unit comes without any connectors, so we don't need to care about suppressing
data transport (if we wish to have data transport, we must specify the
to-be-accessed connectors in the use_named creation dialog window).
Class containers
A container with a collection of named subunits with related functions
resembles in many ways the concept of a class: each subunit offers
a particular "method" that can be invoked via the unit's path name
from outside. Therefore, in NST such a construction is called a "class
container" and the subunits are referred to as its "method subunits"
. Usually, it is not desireable that execution of the class container executes
all of its methods; therefore, a class container usually contains as its
first subunit a return_unit
that protects
the remaining method subunits from being executed with the class container
(they are then only executable from outside via their path names, by means
of a use_method, use_named
or suitably programmed prog_unit).
Here is an example#3 with
a class container named "vec". It offers three methods: "vec:plot"
plots a given vector in graphics window that will open when the method
subunit is executed. "vec:print" will show the values of a given
vector in a window that will open when the method subunit is executed,
and finally "vec:end" will close any window(s) opened by any of
the other two methods. The first method is implemented with three units
inside a container unit, the second method is just the print_vec
unit, and the third method is the ctrl_op
unit that sends -- when invoked -- a control call to the other two units
to close their windows, if they are open. To test the example, the top-level
circuit contains three use_named
units to invoke the methods and a rnd_gen
instance as an example data source.
(click at the 'x'-fields of the method invokers to see the effect).
How to
create class containers with the prog_unit
A particularly simple way to make a class container is offered by the prog_unit:
whenever you declare in the prog_unit code a parameterles routine
with the non-standard "return type" public (which has the same semantics
as "void"), the prog_unit will become a class container with each
such declared routine appearing as a method subunit (the protective return_unit
is automatic in this case and will not be shown by Neo). Public routines
can have static variables declared with the special keywords INP
and OUT (which have the same semantics as "static"). Such
variables will then appear as input or output connectors for the associated
method subunit, analogously as for the main program. Example definition
of a "mini-method" with two input and one output connector, each of a
single float:
...
public sum() {
INP
float x; INP float y; OUT float z;
z
= x+y;
}
...
Since all public routines inside a prog_unit instance will share
any global variables, this is a very convenient scheme for the rapid prototyping
of class containers with methods that operate on a shared set of data.
Many NST circuits are structured in this way: one or a few prog_units
implement the main methods operating on one or a few central data structures
(held in the prog_unit instance(s)). The remaining "machinery" (visualization
and GUI units) are assembled around this functional core, using use_method
or use_named units to invoke the methods implemented in the class
containers. Example#4 illustrates
this technique.
Later, when a compiled C or C++ implementation of the same class container
is desired, NST offers the necessary means to wrap a corresponding C or
C++ class suitably.
How
to turn containers into operators by means of virtual units
We have already seen how to turn a prog_unit into an operator unit that
acts on a number of operands, using the exec_opnds() function. The virtual_for_op
unit offers a similar facility for a container unit: when a virtual_for_op
is placed inside a container, the entire container will appear for the
outside circuit like a for_op unit with an adjustable scope rectangle.
The operands in that scope rectangle (which are successor units to the
container unit) will be executed at the moment when the virtual_for_op
becomes executed inside the container. Moreover, the virtual_for_op
offers inside the container a second scope rectangle, where it can
have additional operand units (this time its immediate successors inside
the container). When the virtual_for_op is executed, it will first execute
all of its direct
(inside the container) operands, then the operands in the outside scope
(the successors to its enclosing container). This will happen
for each iteration, with the number of iterations specified in an analogous
manner as for the usual for_op. Here is a simple example#5
where you can try out the possibilities.
There can be more than a single virtual_for_op inside a container, but
then one should take care that the number of outer operands is for all
virtual_for_ops
the same (this is currently not automatically enforced and the scope rectangle
will show only the scope of one of the virtual_for_ops).
The virtual_unit
is similar to the virtual_for_op, however, it just takes a single
operand (identified by a positional parameter, with
values 1,2,.. selecting successor positions, and -1,-2,... selecting
predecessor positions relative to the enclosing container) and executes
it only once for each execution of the virtual_unit. Data exchange
with the referenced unit can be made by specifying the interface positions
of the connectors or pins to be accessed (this is similar as for the use_named
unit, but the target unit need not be named). The action is true "operand
style", i.e., there will be no double execution from a Step command.
One attractive use of virtual_unit's is the implementation of various
"inspector"
operators: these are just put in front of another unit to display some
information about the operand (which, of course, must be of a type that
can be handled by the inspector operator).