I had a use case for units, a rarely-used feature of Racket, this
week. I ran into units before when hacking on the web server, so I knew what they were and
how they worked, but I had never had occasion to write my own unit
before, except to implement the tcp@ signature.
Basically, units let you write code that depends on bindings whose concrete implementations are filled in at a later point in time. To specify the inputs and outputs of a unit, we use signatures:
(define-signature x^
  (x))
The example above declares a signature containing one binding, x.
Depending on how the signature is used, x may be an import to a unit,
or an export of a unit. We can define a unit, printer@ that takes the
x^ signature as input and exports a procedure to print the value of
x as follows:
(define-signature printer^
  (print-x))
(define-unit printer@
  (import x^)
  (export printer^)
  (define (print-x)
    (println x)))
What this is saying is that once the unit printer@ is invoked, it will
provide a procedure named print-x that refers to whatever binding of
x is available in scope at invocation time. To invoke the unit, we
can use define-values/invoke-unit:
(define x 42) ;; provide a definition for `x` to be used by the unit
(define-values/invoke-unit printer@
  (import x^)
  (export doer^))
(print-x) ;; prints 42
In Congame, we have a DSL for building studies called #lang conscript which provides some syntactic conveniences on top of
racket/base. To run a Conscript study, you need a Congame server,
which can be a pain to set up, especially for students. So, we have
another language called #lang conscript/local, which re-provides the
bindings from #lang conscript, replacing some of them so that the
whole thing works using a stand-alone web server that doesn't require a
running Postgres database. My use case for units was to write a generic
implentation of matchmaking that is reusable between the
two languages.
The process was straightforward. I wrote a signature for the things that a Conscript-like language provides:
(define-signature conscript^
  (get-var put-var undefined? call-with-study-transaction)) ;; among others
Then, I wrote the signature for the matchmaking implementation:
(define-signature matchmaking^
  (get-ready-groups get-current-group matchmake))
Then, I implemented the unit:
(define-unit matchmaking@
  (import conscript^)
  (export matchmaking^)
  (define (get-ready-groups) ...)
  (define (get-current-group) ...)
  (define (matchmake group-size) ...))
Finally, I instantiated it once using the bindings provided by #lang conscript:
#lang conscript
(require "matchmaking-sig.rkt"
         "matchmaking-unit.rkt")
(provide (all-defined-out))
(define-values/invoke-unit matchmaking@
  (import conscript^)
  (export matchmaking^))
And then I did the same for the conscript/local version in a different
module. The only difference between the two modules is the #lang
line. The end result is that users of the two languages can import the
respective matchmaking module for their #lang and get implementations
that work for their environment, and I get to maintain a single generic
implementation between the two languages.