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.
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:
require-expression can be a
list of two elements, the first being the symbol file and the second
a string giving the name of the file. (You'll need to give the full path
unless the file is located in the same folder as the program containing the
require-expression.)require-expression can be a list in which the
first element is the symbol lib, the second is a string giving the
name of a file, and the remaining elements are strings that identify the
collection to which the file belongs.
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.
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.
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.