Modules

Course links

Structuring large programs

Most programmers, as the programs they write grow larger and more intricate, find it convenient to structure their programs, dividing the source code into files that can be read and revised separately, each file containing a collection of definitions and commands that share a common theme. For instance, a module might contain definitions for the constructor, type predicate, selectors, mutators, copier, and displayer for a record of some kind, or a collection of definitions of procedures that compute hyperbolic trigonometric functions, or procedures for laying out a particular graphical user interface. When one is working with a large program, it is easier to store each group of related procedures in a separate file than to scroll repeatedly up and down in a really long file, searching for the group that one needs to work on or look at next.

The load procedure in the current Scheme standard provides a little support for this idea of dividing a long program into separate files. Executing a program that includes calls to load will cause the definitions stored in the files that are loaded to be, in effect, added to the program and the commands stored in those files to be executed. Moreover, a file that is loaded can itself include calls to load, so that the programmer can, in effect, organize her program in layers, with the files in each layer loading files from the layer beneath.

Modules in PLT Scheme

However, PLT Scheme has a more expressive and subtle mechanism for the same purpose: modules. Any collection of definitions, syntax definitions, and commands can be bundled together into a module and given a single name. Here's an example in which the module is designed to provide a program that uses it with three related procedures (factorial, choose, and Catalan). This module also contains definitions for other incidental procedures, as well as a syntax definition for assert-expressions.

(module binomial-coefficients mzscheme

  ;; assert -- enforce a condition

  (define-syntax assert
    (syntax-rules ()
      ((_ condition)
       (if (not condition)
           (error "Assertion failed: " 'condition)))))

  ;; natural-number? -- test whether a given value is an exact non-negative
  ;; integer

  (define natural-number?
    (lambda (something)
      (and (integer? something)
           (exact? something)
           (not (negative? something)))))

  ;; safe-factorial -- compute the product of all positive integers less
  ;; than or equal to a given natural number

  (define safe-factorial
    (lambda (n)
      (let kernel ((so-far 1)
                   (multiplier n))
        (if (zero? multiplier)
            so-far
            (kernel (* so-far multiplier) (- multiplier 1))))))

  ;; factorial -- compute the product of all positive integers less than or
  ;; equal to a given natural number 

  (define factorial
    (lambda (n)
      (assert (natural-number? n))
      (safe-factorial n)))

  ;; safe-choose -- compute the number of ways of selecting k objects from
  ;; a set of n objects

  (define safe-choose
    (lambda (n k)
      (/ (safe-factorial n) (safe-factorial k) (safe-factorial (- n k)))))

  ;; choose -- compute the number of ways of selecting k objects from a set
  ;; of n objects

  (define choose
    (lambda (n k)
      (assert (natural-number? n))
      (assert (natural-number? k))
      (assert (<= k n))
      (safe-choose n k)))

  ;; Catalan -- compute the number of differently configured pair
  ;; structures containing n cons cells

  (define Catalan
    (lambda (n)
      (assert (natural-number? n))
      (/ (safe-choose (* 2 n) n) (+ n 1))))

  (provide factorial choose Catalan))

The left parenthesis at the beginning of the first line matches the right parenthesis at the end of the last line, so this is all one big module declaration. It begins with the keyword module, the name that the programmer gives to the module (in this case, binomial-coefficients), and the name of the particular sublanguage of PLT Scheme in which the module's body is written (in this case, mzscheme). At the end, the programmer adds a provide-expression; its subexpressions are the identifiers and keywords that the module contributes to any program that invokes the module. In this case, the programmer has chosen to ``export'' the factorial, choose, and Catalan procedures.

Usually a module declaration is stored by itself in a file, and the file is named after the module, with the addition of an appropriate suffix (.ss or .scm, whichever the programmer prefers). The module shown above, for instance, should be saved in a file called binomial-coefficients.ss.

To incorporate a module that has already been written and saved in a file into a larger program, one uses a require-expression in that program. A single require-expression can include any number of subexpressions, called module names, each of which should tell how to find a module in one of two ways:

For instance, the require-expression

(require (file "/home/stone/courses/scheme/examples/binomial-coefficients.ss")
         (lib "cgi.ss" "net"))

would invoke both the binomial-coefficients module shown above and the cgi module from the PLT Scheme net collection.

When a program invokes a module, the identifiers that the module provides become defined in the program, with the values (or transformers) that the module gives them. In addition, any commands that appear inside the module are executed. It is possible for a module to invoke other modules. However, Scheme remembers which modules it has already invoked in a given program, and silently skips the invocation of any module that has already been invoked at least once.

Variants of require

The require-expressions described above are actually just the simplest of five variants.

When invoking a module, a program can specify that, instead of receiving all of the identifiers provided by that module, it wants to exclude some while still receiving the rest. To achieve this, the programmer would use a require-specification (that is, a subexpression of a require-expression) that, instead of being simply a module name, is a list in which the first element is the symbol all-except, the second is the module name, and all subsequent elements are identifiers that are not to be imported from the module, even if the module tries to provide them. For instance,

(require (all-except
            (file "/home/stone/courses/scheme/examples/binomial-coefficients.ss")
            Catalan))

would cause the definitions of factorial and choose to be brought in from the module, but not Catalan.

Alternatively, a program can request just one identifier from a module, by using a require-specification that is a list of four elements: the symbol rename, a module name, the local identifier that the program will use for the item being imported, and the identifier that the module used for that item in its provide-expression. The last two can be the same, but they may also differ if the programmer has a name that he, for whatever reason, prefers to use.

One common reason for performing such a renaming is that the program is already using, for some unrelated purpose, the identifier that the module provides. This happens so frequently, in fact, that programmers often find it convenient to rename, systematically, all of the items that a module provides, often by adding some fixed prefix to all of the imported identifiers, indicating their origin. The require-specification that achieves this is a three-element list in which the first element is the symbol prefix, the second is the desired prefix (as a symbol), and the third is a module name. For instance,

(require (prefix bc:
            (file "/home/stone/courses/scheme/examples/binomial-coefficients.ss")))

would import all three of the procedures that the binomial-coefficients module provides, but would rename them bc:factorial, bc:choose, and bc:Catalan.

Variants of provide

Similarly, the subexpressions of the provide-expression at the end of a module need not be simple identifiers. There are several other kinds of provide-specifications, of which I'll mention three:

If a provide-specification is a list consisting of the symbol rename, an identifier that names a value or transformer defined within the module, and another identifier, then the item is provided, but under a different name than the module itself uses for it. So, for instance,

(provide (rename factorial facto))

would export the factorial procedure, but the program in which it is used will have to call it facto (unless the require-expression that invokes the module renames facto again).

The provide-specification (all-defined) causes all the items for which the module contains definitions or syntax definitions to be exported.

If a provide-specification is a list in which the first element is the symbol all-defined-except and the other elements are identifiers, then the module exports all of the items except those named.