28 KiB
Units Components
Units 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 called 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.
1 Signatures and Units
2 Invoking Units
3 Linking Units
4 First-Class Units
5 Whole-`module` Signatures and Units
6 Contracts for Units
6.1 Adding Contracts to Signatures
6.2 Adding Contracts to Units
7 `unit` versus `module`
1. Signatures and Units
The interface of a unit is described in terms of signatures. Each
signature is defined normally within a `module`
using
define-signature
. For example, the following signature, placed in a
"toy-factory-sig.rkt"
file, describes the exports of a component that
implements a toy factory:
By convention, signature names end with
^
.
"toy-factory-sig.rkt"
#lang racket
(define-signature toy-factory^
(build-toys ; (integer? -> (listof toy?))
repaint ; (toy? symbol? -> toy?)
toy? ; (any/c -> boolean?)
toy-color)) ; (toy? -> symbol?)
(provide toy-factory^)
An implementation of the toy-factory^
signature is written using
define-unit
with an export
clause that names toy-factory^
:
By convention, unit names end with
@
.
"simple-factory-unit.rkt"
#lang racket
(require "toy-factory-sig.rkt")
(define-unit simple-factory@
(import)
(export toy-factory^)
(printf "Factory started.\n")
(define-struct toy (color) #:transparent)
(define (build-toys n)
(for/list ([i (in-range n)])
(make-toy 'blue)))
(define (repaint t col)
(make-toy col)))
(provide simple-factory@)
The toy-factory^
signature also could be referenced by a unit that
needs a toy factory to implement something else. In that case,
toy-factory^
would be named in an import
clause. For example, a toy
store would get toys from a toy factory. (Suppose, for the sake of an
example with interesting features, that the store is willing to sell
only toys in a particular color.)
"toy-store-sig.rkt"
#lang racket
(define-signature toy-store^
(store-color ; (-> symbol?)
stock! ; (integer? -> void?)
get-inventory)) ; (-> (listof toy?))
(provide toy-store^)
"toy-store-unit.rkt"
#lang racket
(require "toy-store-sig.rkt"
"toy-factory-sig.rkt")
(define-unit toy-store@
(import toy-factory^)
(export toy-store^)
(define inventory null)
(define (store-color) 'green)
(define (maybe-repaint t)
(if (eq? (toy-color t) (store-color))
t
(repaint t (store-color))))
(define (stock! n)
(set! inventory
(append inventory
(map maybe-repaint
(build-toys n)))))
(define (get-inventory) inventory))
(provide toy-store@)
Note that "toy-store-unit.rkt"
imports "toy-factory-sig.rkt"
, but
not "simple-factory-unit.rkt"
. Consequently, the toy-store@
unit
relies only on the specification of a toy factory, not on a specific
implementation.
2. Invoking Units
The simple-factory@
unit has no imports, so it can be invoked directly
using invoke-unit
:
> (require "simple-factory-unit.rkt")
> (invoke-unit simple-factory@)
Factory started.
The invoke-unit
form does not make the body definitions available,
however, so we cannot build any toys with this factory. The
define-values/invoke-unit
form binds the identifiers of a signature to
the values supplied by a unit to be invoked
that implements the
signature:
> (define-values/invoke-unit/infer simple-factory@)
Factory started.
> (build-toys 3)
(list (toy 'blue) (toy 'blue) (toy 'blue))
Since simple-factory@
exports the toy-factory^
signature, each
identifier in toy-factory^
is defined by the
define-values/invoke-unit/infer
form. The /infer
part of the form
name indicates that the identifiers bound by the declaration are
inferred from simple-factory@
.
Now that the identifiers in toy-factory^
are defined, we can also
invoke toy-store@
, which imports toy-factory^
to produce
toy-store^
:
> (require "toy-store-unit.rkt")
> (define-values/invoke-unit/infer toy-store@)
> (get-inventory)
'()
> (stock! 2)
> (get-inventory)
(list (toy 'green) (toy 'green))
Again, the /infer
part define-values/invoke-unit/infer
determines
that toy-store@
imports toy-factory^
, and so it supplies the
top-level bindings that match the names in toy-factory^
as imports to
toy-store@
.
3. Linking Units
We can make our toy economy more efficient by having toy factories that
cooperate with stores, creating toys that do not have to be repainted.
Instead, the toys are always created using the store’s color, which the
factory gets by importing toy-store^
:
"store-specific-factory-unit.rkt"
#lang racket
(require "toy-store-sig.rkt"
"toy-factory-sig.rkt")
(define-unit store-specific-factory@
(import toy-store^)
(export toy-factory^)
(define-struct toy () #:transparent)
(define (toy-color t) (store-color))
(define (build-toys n)
(for/list ([i (in-range n)])
(make-toy)))
(define (repaint t col)
(error "cannot repaint")))
(provide store-specific-factory@)
To invoke store-specific-factory@
, we need toy-store^
bindings to
supply to the unit. But to get toy-store^
bindings by invoking
toy-store@
, we will need a toy factory! The unit implementations are
mutually dependent, and we cannot invoke either before the other.
The solution is to link the units together, and then we can invoke the
combined units. The define-compound-unit/infer
form links any number
of units to form a combined unit. It can propagate imports and exports
from the linked units, and it can satisfy each unit’s imports using the
exports of other linked units.
> (require "toy-factory-sig.rkt")
> (require "toy-store-sig.rkt")
> (require "store-specific-factory-unit.rkt")
> (define-compound-unit/infer toy-store+factory@
(import)
(export toy-factory^ toy-store^)
(link store-specific-factory@
toy-store@))
The overall result above is a unit toy-store+factory@
that exports
both toy-factory^
and toy-store^
. The connection between
store-specific-factory@
and toy-store@
is inferred from the
signatures that each imports and exports.
This unit has no imports, so we can always invoke it:
> (define-values/invoke-unit/infer toy-store+factory@)
> (stock! 2)
> (get-inventory)
(list (toy) (toy))
> (map toy-color (get-inventory))
'(green green)
4. First-Class Units
The define-unit
form combines define
with a unit
form, similar to
the way that (define (f x) ....)
combines define
followed by an
identifier with an implicit lambda
.
Expanding the shorthand, the definition of toy-store@
could almost be
written as
(define toy-store@
(unit
(import toy-factory^)
(export toy-store^)
(define inventory null)
(define (store-color) 'green)
....))
A difference between this expansion and define-unit
is that the
imports and exports of toy-store@
cannot be inferred. That is, besides
combining define
and unit
, define-unit
attaches static information
to the defined identifier so that its signature information is available
statically to define-values/invoke-unit/infer
and other forms.
Despite the drawback of losing static signature information, unit
can
be useful in combination with other forms that work with first-class
values. For example, we could wrap a unit
that creates a toy store in
a lambda
to supply the store’s color:
"toy-store-maker.rkt"
#lang racket
(require "toy-store-sig.rkt"
"toy-factory-sig.rkt")
(define toy-store@-maker
(lambda (the-color)
(unit
(import toy-factory^)
(export toy-store^)
(define inventory null)
(define (store-color) the-color)
; the rest is the same as before
(define (maybe-repaint t)
(if (eq? (toy-color t) (store-color))
t
(repaint t (store-color))))
(define (stock! n)
(set! inventory
(append inventory
(map maybe-repaint
(build-toys n)))))
(define (get-inventory) inventory))))
(provide toy-store@-maker)
To invoke a unit created by toy-store@-maker
, we must use
define-values/invoke-unit
, instead of the /infer
variant:
> (require "simple-factory-unit.rkt")
> (define-values/invoke-unit/infer simple-factory@)
Factory started.
> (require "toy-store-maker.rkt")
> (define-values/invoke-unit (toy-store@-maker 'purple)
(import toy-factory^)
(export toy-store^))
> (stock! 2)
> (get-inventory)
(list (toy 'purple) (toy 'purple))
In the define-values/invoke-unit
form, the (import toy-factory^)
line takes bindings from the current context that match the names in
toy-factory^
(the ones that we created by invoking
simple-factory@
), and it supplies them as imports to toy-store@
.
The (export toy-store^)
clause indicates that the unit produced by
toy-store@-maker
will export toy-store^
, and the names from that
signature are defined after invoking the unit.
To link a unit from toy-store@-maker
, we can use the compound-unit
form:
> (require "store-specific-factory-unit.rkt")
> (define toy-store+factory@
(compound-unit
(import)
(export TF TS)
(link [((TF : toy-factory^)) store-specific-factory@ TS]
[((TS : toy-store^)) toy-store@ TF])))
This compound-unit
form packs a lot of information into one place. The
left-hand-side TF
and TS
in the link
clause are binding
identifiers. The identifier TF
is essentially bound to the elements of
toy-factory^
as implemented by store-specific-factory@
. The
identifier TS
is similarly bound to the elements of toy-store^
as
implemented by toy-store@
. Meanwhile, the elements bound to TS
are
supplied as imports for store-specific-factory@
, since TS
follows
store-specific-factory@
. The elements bound to TF
are similarly
supplied to toy-store@
. Finally, (export TF TS)
indicates that the
elements bound to TF
and TS
are exported from the compound unit.
The above compound-unit
form uses store-specific-factory@
as a
first-class unit, even though its information could be inferred. Every
unit can be used as a first-class unit, in addition to its use in
inference contexts. Also, various forms let a programmer bridge the gap
between inferred and first-class worlds. For example,
define-unit-binding
binds a new identifier to the unit produced by an
arbitrary expression; it statically associates signature information to
the identifier, and it dynamically checks the signatures against the
first-class unit produced by the expression.
5. Whole-module
Signatures and Units
In programs that use units, modules like "toy-factory-sig.rkt"
and
"simple-factory-unit.rkt"
are common. The racket/signature
and
racket/unit
module names can be used as languages to avoid much of the
boilerplate module, signature, and unit declaration text.
For example, "toy-factory-sig.rkt"
can be written as
#lang racket/signature
build-toys ; (integer? -> (listof toy?))
repaint ; (toy? symbol? -> toy?)
toy? ; (any/c -> boolean?)
toy-color ; (toy? -> symbol?)
The signature toy-factory^
is automatically provided from the module,
inferred from the filename "toy-factory-sig.rkt"
by replacing the
"-sig.rkt"
suffix with ^
.
Similarly, "simple-factory-unit.rkt"
module can be written
#lang racket/unit
(require "toy-factory-sig.rkt")
(import)
(export toy-factory^)
(printf "Factory started.\n")
(define-struct toy (color) #:transparent)
(define (build-toys n)
(for/list ([i (in-range n)])
(make-toy 'blue)))
(define (repaint t col)
(make-toy col))
The unit simple-factory@
is automatically provided from the module,
inferred from the filename "simple-factory-unit.rkt"
by replacing the
"-unit.rkt"
suffix with @
.
6. Contracts for Units
There are a couple of ways of protecting units with contracts. One way is useful when writing new signatures, and the other handles the case when a unit must conform to an already existing signature.
6.1. Adding Contracts to Signatures
When contracts are added to a signature, then all units which implement
that signature are protected by those contracts. The following version
of the toy-factory^
signature adds the contracts previously written in
comments:
"contracted-toy-factory-sig.rkt"
#lang racket
(define-signature contracted-toy-factory^
((contracted
[build-toys (-> integer? (listof toy?))]
[repaint (-> toy? symbol? toy?)]
[toy? (-> any/c boolean?)]
[toy-color (-> toy? symbol?)])))
(provide contracted-toy-factory^)
Now we take the previous implementation of simple-factory@
and
implement this version of toy-factory^
instead:
"contracted-simple-factory-unit.rkt"
#lang racket
(require "contracted-toy-factory-sig.rkt")
(define-unit contracted-simple-factory@
(import)
(export contracted-toy-factory^)
(printf "Factory started.\n")
(define-struct toy (color) #:transparent)
(define (build-toys n)
(for/list ([i (in-range n)])
(make-toy 'blue)))
(define (repaint t col)
(make-toy col)))
(provide contracted-simple-factory@)
As before, we can invoke our new unit and bind the exports so that we can use them. This time, however, misusing the exports causes the appropriate contract errors.
> (require "contracted-simple-factory-unit.rkt")
> (define-values/invoke-unit/infer contracted-simple-factory@)
Factory started.
> (build-toys 3)
(list (toy 'blue) (toy 'blue) (toy 'blue))
> (build-toys #f)
build-toys: contract violation
expected: integer?
given: #f
in: the 1st argument of
(-> integer? (listof toy?))
contract from:
(unit contracted-simple-factory@)
blaming: top-level
(assuming the contract is correct)
at: eval:34.0
> (repaint 3 'blue)
repaint: contract violation
expected: toy?
given: 3
in: the 1st argument of
(-> toy? symbol? toy?)
contract from:
(unit contracted-simple-factory@)
blaming: top-level
(assuming the contract is correct)
at: eval:34.0
6.2. Adding Contracts to Units
However, sometimes we may have a unit that must conform to an already
existing signature that is not contracted. In this case, we can create
a unit contract with unit/c
or use the define-unit/contract
form,
which defines a unit which has been wrapped with a unit contract.
For example, here’s a version of toy-factory@
which still implements
the regular toy-factory^
, but whose exports have been protected with
an appropriate unit contract.
"wrapped-simple-factory-unit.rkt"
#lang racket
(require "toy-factory-sig.rkt")
(define-unit/contract wrapped-simple-factory@
(import)
(export (toy-factory^
[build-toys (-> integer? (listof toy?))]
[repaint (-> toy? symbol? toy?)]
[toy? (-> any/c boolean?)]
[toy-color (-> toy? symbol?)]))
(printf "Factory started.\n")
(define-struct toy (color) #:transparent)
(define (build-toys n)
(for/list ([i (in-range n)])
(make-toy 'blue)))
(define (repaint t col)
(make-toy col)))
(provide wrapped-simple-factory@)
> (require "wrapped-simple-factory-unit.rkt")
> (define-values/invoke-unit/infer wrapped-simple-factory@)
Factory started.
> (build-toys 3)
(list (toy 'blue) (toy 'blue) (toy 'blue))
> (build-toys #f)
wrapped-simple-factory@: contract violation
expected: integer?
given: #f
in: the 1st argument of
(unit/c
(import)
(export (toy-factory^
(build-toys
(-> integer? (listof toy?)))
(repaint (-> toy? symbol? toy?))
(toy? (-> any/c boolean?))
(toy-color (-> toy? symbol?))))
(init-depend))
contract from:
(unit wrapped-simple-factory@)
blaming: top-level
(assuming the contract is correct)
at: <collects>/racket/unit.rkt
> (repaint 3 'blue)
wrapped-simple-factory@: contract violation
expected: toy?
given: 3
in: the 1st argument of
(unit/c
(import)
(export (toy-factory^
(build-toys
(-> integer? (listof toy?)))
(repaint (-> toy? symbol? toy?))
(toy? (-> any/c boolean?))
(toy-color (-> toy? symbol?))))
(init-depend))
contract from:
(unit wrapped-simple-factory@)
blaming: top-level
(assuming the contract is correct)
at: <collects>/racket/unit.rkt
7. unit
versus module
As a form for modularity, unit
complements module
:
-
The
module
form is primarily for managing a universal namespace. For example, it allows a code fragment to refer specifically to thecar
operation fromracket/base
—the one that extracts the first element of an instance of the built-in pair datatype—as opposed to any number of other functions with the namecar
. In other words, themodule
construct lets you refer to the binding that you want. -
The
unit
form is for parameterizing a code fragment with respect to most any kind of run-time value. For example, it allows a code fragment to work with acar
function that accepts a single argument, where the specific function is determined later by linking the fragment to another. In other words, theunit
construct lets you refer to a binding that meets some specification.
The lambda
and class
forms, among others, also allow
parameterization of code with respect to values that are chosen later.
In principle, any of those could be implemented in terms of any of the
others. In practice, each form offers certain conveniences—such as
allowing overriding of methods or especially simple application to
values—that make them suitable for different purposes.
The module
form is more fundamental than the others, in a sense. After
all, a program fragment cannot reliably refer to a lambda
, class
, or
unit
form without the namespace management provided by module
. At
the same time, because namespace management is closely related to
separate expansion and compilation, module
boundaries end up as
separate-compilation boundaries in a way that prohibits mutual
dependencies among fragments. For similar reasons, module
does not
separate interface from implementation.
Use unit
when module
by itself almost works, but when separately
compiled pieces must refer to each other, or when you want a stronger
separation between interface (i.e., the parts that need to be known
at expansion and compilation time) and implementation (i.e., the
run-time parts). More generally, use unit
when you need to
parameterize code over functions, datatypes, and classes, and when the
parameterized code itself provides definitions to be linked with other
parameterized code.