14.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 [email protected] could almost be written as

(define [email protected]
   (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 [email protected] 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:


#lang racket
(require "toy-store-sig.rkt"
(define [email protected]
  (lambda (the-color)
     (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))
           (repaint t (store-color))))
     (define (stock! n)
       (set! inventory
             (append inventory
                     (map maybe-repaint
                          (build-toys n)))))
     (define (get-inventory) inventory))))
(provide [email protected])

To invoke a unit created by [email protected], we must use define-values/invoke-unit, instead of the /infer variant:

> (require "simple-factory-unit.rkt")
> (define-values/invoke-unit/infer [email protected])

Factory started.

> (require "toy-store-maker.rkt")
> (define-values/invoke-unit ([email protected] '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 [email protected]), and it supplies them as imports to [email protected]. The (export toy-store^) clause indicates that the unit produced by [email protected] will export toy-store^, and the names from that signature are defined after invoking the unit.

To link a unit from [email protected], we can use the compound-unit form:

> (require "store-specific-factory-unit.rkt")
> (define [email protected]
     (export TF TS)
     (link [((TF : toy-factory^)) [email protected] TS]
           [((TS : toy-store^)) [email protected] 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 [email protected]. The identifier TS is similarly bound to the elements of toy-store^ as implemented by [email protected]. Meanwhile, the elements bound to TS are supplied as imports for store[email protected], since TS follows [email protected]. The elements bound to TF are similarly supplied to [email protected]. 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 [email protected] 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.