MzScheme's units are used to organize a program into separately compilable and reusable components. A unit resembles a procedure in that both are first-class values that are used for abstraction. While procedures abstract over values in expressions, units abstract over names in collections of definitions. Just as a procedure is invoked to evaluate its expressions given actual arguments for its formal parameters, a unit is invoked to evaluate its definitions given actual references for its imported variables. Unlike a procedure, however, a unit's imported variables can be partially linked with the exported variables of another unit prior to invocation. Linking merges multiple units together into a single compound unit. The compound unit itself imports variables that will be propagated to unresolved imported variables in the linked units, and re-exports some variables from the linked units for further linking.
In some ways, a unit resembles a module (see Chapter 5 in PLT MzScheme: Language Manual), but
units and modules serve different purposes overall. A unit
encapsulates a pluggable component -- code that relies, for example, on
``some function f
from a source to be determined later.'' In
contrast, if a module imports a function, the import is ``the
function f
provided by the specific module m
.'' Moreover,
a unit is a first-class value that can be multiply instantiated, each
time with different imports, whereas a module's context is
fixed. Finally, because a unit's interface is separate from its
implementation, units naturally support mutually recursive references
across unit boundaries, while module imports must be acyclic.
MzScheme supports two layers of units. The core unit system
comprises the unit
, compound-unit
, and
invoke-unit
syntactic forms. These forms implement the basic
mechanics of units for separate compilation and linking. While the
semantics of units is most easily understood via the core forms, they
are too verbose for specifying the interconnections between units in
a large program. Therefore, a system of units with
signatures is provided on top of the core forms, comprising the
define-signature
, unit/sig
,
compound-unit/sig
, and invoke-unit/sig
syntactic
forms.
The core system is described in this chapter, and defined by the unit.ss library. The signature system is described in section 41, and defined by unitsig.ss. Details about mixing core and signed units are presented in section 41.9 (using procedures from unitsig.ss).
(unit (import variable ···) (export exportage ···) unit-body-expr ···) exportage is one of variable (internal-variable external-variable)
The variable
s in the import
clause are bound
within the unit-body-expr
expressions. The variables for
exportage
s in the export
clause must be defined
in the unit-body-expr
s as described below; additional private
variables can be defined as well. The imported and exported variables
cannot occur on the left-hand side of an assignment (i.e., a
set!
expression).
The first exportage
form exports the binding defined as
variable
in the unit body using the external name
variable
. The second form exports the binding defined as
internal-variable
using the external name
external-variable
. The external variables from an
export
clause must be distinct.
Each exported variable
or internal-variable
must be
defined in a define-values
expression as a
unit-body-expr
.11 All
identifiers defined by the unit-body-expr
s together with the
variable
s from the import
clause must be distinct.
The unit defined below imports and exports no variables. Each time it is invoked, it prints and returns the current time in seconds:12
(define f1@ (unit (import) (export) (define x (current-seconds
)) (display
x) (newline
) x))
The unit defined below is similar, except that it exports the variable
x
instead of returning the value:
(define f2@ (unit (import) (export x) (define x (current-seconds
)) (display
x) (newline
)))
The following units define two parts of an interactive phone book:
(define database@ (unit (import show-message) (export insert lookup) (define table (list
)) (define insert (lambda (name info) (set! table (cons
(cons
name info) table)))) (define lookup (lambda (name) (let ([data (assoc name table)]) (if data (cdr
data) (show-message "info not found"))))) insert)) (define interface@ (unit (import insert lookup make-window make-button) (export show-message) (define show-message (lambda (msg) ...)) (define main-window ...)))
In this example, the database@
unit implements the
database-searching part of the program, and the interface@
unit implements the graphical user interface. The database@
unit exports insert
and lookup
procedures to be
used by the graphical interface, while the interface@
unit
exports a show-message
procedure to be used by the database
(to handle errors). The interface@
unit also imports
variables that will be supplied by an platform-specific graphics
toolbox.
A unit is invoked using the invoke-unit
form:
(invoke-unit unit-expr import-expr ···)
The value of unit-expr
must be a unit. For each of the unit's
imported variables, the invoke-unit
expression must contain
an import-expr
. The value of each import-expr
is imported
into the unit. More detailed information about linking is provided in
the following section on compound units.
Invocation proceeds in two stages. First, invocation creates bindings
for the unit's private, imported, and exported variables. All
bindings are initialized to the undefined value. Second,
invocation evaluates the unit's private definitions and
expressions. The result of the last expression in the unit is the
result of the invoke-unit
expression. The unit's exported
variable bindings are not accessible after the invocation.
These examples use the definitions from the earlier unit examples in section 40.1.
The f1@
unit is invoked with no imports:
(invoke-unit f1@) ; => displays and returns the current time
Here is one way to invoke the database@
unit:
(invoke-unit database@ display
)
This invocation links the imported variable message
in
database@
to the standard Scheme
procedure,
sets up an empty database, and creates the procedures display
insert
and lookup
tied to this particular database. Since the last
expression in the database@
unit is insert
, the
invoke-unit
expression returns the insert
procedure
(without binding any top-level variables). The fact that
insert
and lookup
are exported is irrelevant to the
invocation; exports are only used for linking.
Invoking the database@
unit directly in the above manner is
actually useless. Although a program can insert information into the
database, it cannot extract information since the lookup
procedure is not accessible. The database@
unit becomes
useful when it is linked with another unit in a
compound-unit
expression.
(
SYNTAX
define-values/invoke-unit
)(
export-id ···)
unit-expr [prefix import-id ···]
This form is similar to invoke-unit
. However, instead of
returning the value of the unit's initialization expression,
define-values/invoke-unit
expands to a
define-values
expression that binds each identifier
export-id
to the value of the corresponding variable exported
by the unit. At run time, if the unit does not export all of the
export-id
s, the exn:fail:unit
exception is raised.
If prefix
is specified, it must be either #f
or an
identifier. If it is an identifier, the names defined by the
expansion of define-values/invoke-unit
are prefixed with
.prefix
:
Example:
(define x 3) (define y 2) (define-values/invoke-unit (c) (unit (import a b) (export c) (define c (- a b))) ex x y) ex:c ; => 1
(
SYNTAX
namespace-variable-bind/invoke-unit
)(
export-id ···)
unit-expr [prefix import-id ···]
This form is like define-values/invoke-unit
, but the expansion
is a sequence of calls to namespace-set-variable-value!
instead of a
define-values
expression. Thus, when it is evaluated, a
namespace-variable-bind/invoke-unit
expression binds top-level
variables in the current namespace.
The compound-unit
form links several units into one new
compound unit. In the process, it matches imported variables in each
sub-unit either with exported variables of other sub-units or with
its own imported variables:
(compound-unit (import variable ···) (link (tag (sub-unit-expr linkage ···)) ···) (export (tag exportage ···) ···)) linkage is one of variable (tag variable) (tag variable ···) exportage is one of variable (internal-variable external-variable) tag is identifier
The three parts of a compound-unit
expression have the
following roles:
The import
clause imports variables into the
compound unit. These imported variables are used as imports to the
compound unit's sub-units.
The link
clause specifies how the compound unit
is created from sub-units. A unique tag
is associated with each
sub-unit, which is specified using an arbitrary expression.
Following the unit expression, each linkage
specifies a
variable using the variable
form or the (
form. In the former case, the tag
variable
)variable
must
occur in the import
clause of the compound-unit
expression; in the latter case, the tag
must be defined in the
same compound-unit
expression. The (
form is a shorthand for multiple adjacent
clauses of the second form with the same tag
variable
···)tag
.
The export
clause re-exports variables from the
compound unit that were originally exported from the sub-units. The
tag
part of each export
sub-clause specifies the sub-unit
from which the re-exported variable is drawn. The exportage
s
specify the names of variables exported by the sub-unit to be
re-exported.
As in the export
clause of the unit
form, a re-exported
variable can be renamed for external references using the
(
form. The
internal-variable
external-variable
)internal-variable
is used as the name exported by the sub-unit,
and external-variable
is the name visible outside the compound
unit.
The evaluation of a compound-unit
expression starts with the
evaluation of the link
clause's unit expressions (in
sequence). For each sub-unit, the number of variables it imports must
match the number of linkage
specifications that are provided,
and each linkage
specification is matched to an imported
variable by position. Each sub-unit must also export those variables
that are specified by the link
and export
clauses. If,
for any sub-unit, the number of imported variables does not agree
with the number of linkages provided, the exn:fail:unit
exception is raised. If an
expected exported variable is missing from a sub-unit for linking to
another sub-unit, the exn:fail:unit
exception is raised. If an expected export
variable is missing for re-export, the exn:fail:unit
exception is raised.
The invocation of a compound unit proceeds in two phases to invoke the
sub-units. In the first phase, the compound unit resolves the
imported variables of sub-units with the bindings provided for the
compound unit's imports and new bindings created for sub-unit
exports. In the second phase, the internal definitions and
expressions of the sub-units are evaluated sequentially according to
the order of the sub-units in the link
clause. The result of
invoking a compound unit is the result from the invocation of the
last sub-unit.
These examples use the definitions from the earlier unit examples in section 40.1.
The following compound-unit
expression creates a (probably
useless) renaming wrapping around the unit bound to f2@
:
(define f3@ (compound-unit (import) (link [A (f2@)]) (export (A (x A:x)))))
The only difference between f2@
and f3@
is that
f2@
exports a variable named x
, while f3@
exports a variable named A:x
.
The following example shows how the database@
and
interface@
units are linked together with a graphical
toolbox unit Graphics
to produce a single, fully-linked
compound unit for the interactive phone book program.
(define program@ (compound-unit (import) (link (GRAPHICS (graphics@)) (DATABASE (database@ (INTERFACE show-message))) (INTERFACE (interface@ (DATABASE insert lookup) (GRAPHICS make-window make-button)))) (export)))
This phone book program is executed with (invoke-unit
program@)
. If (invoke-unit program@)
is evaluated a second
time, then a new, independent database and window are created.
(unit?
v
)
returns #t
if v
is a unit or #f
otherwise.
11 The detection of unit definitions is
the same as for internal definitions (see
section 2.8.5 in PLT MzScheme: Language Manual). Thus, the define
and
define-struct
forms can be used for definitions.
12 The ``@
'' in the variable name ``f1@
''
indicates (by convention) that its value is a unit.