Table of Contents
Modules provide an additional level of scoping control, allowing symbolic and syntactic bindings to be bundled in a named or anonymous package. The package can then be imported into any scope, making the bindings contained in the module visible in only that scope.
SISC's modules are provided by the portable syntax-case macro expander by R. Kent Dybvig and Oscar Waddell. A comprehensive explanation of the provided module system is best found in the Chez Scheme Users Guide , specifically Section 9.3, Modules . What follows is an informal introduction to that module system.
The basic unit of modularization in SISC is a module. A typical module definition has this appearance:
(module foo (bar baz) (import boo1) (import boo2) (include "file.scm") (define (bar x) ...) (define-syntax baz ...) (define (something-else ...) ...) (do-something) (do-something-else))
A module definition consists of a name
(foo
), a list of exports
(bar
and baz
) and a
body.
Expressions which can appear in the body of a module are the
same as those which can appear in a
lambda
body. The
import
form imports bindings from a named
module (in this case boo1
and
boo2
) into the current lexical scope. The
include
form performs a textual inclusion
of the source code found in the named file
(file.scm
). In other words, it works as if
the contents of the file had appeared literally in place of
the include
statement.
All identifiers appearing in the export list must be
define
d or
define-syntax
ed in the body of the module,
or import
ed from another module.
It is recommended to clearly separate modularization from actual code. The best way to accomplish this is to
List all imports in the module body rather than in included files
Include all files directly from the module body, avoiding nested includes
Place all definitions and expressions in included files, avoiding them in the module body
There are several reasons for this. First, it makes refactoring easier, as one can move relevant code from module to module merely by rewriting the module definitions, leaving the implementation code unchanged. Second, it makes debugging easier, as one can load the implementation code directly into the Scheme system to have access to all bindings, or load the module definition to view the finished, encapsulated exports. Finally, it stylistically separates interface (the modules) from implementation (the included Scheme source).
Since module bodies are treated like the bodies of
lambda
s, the R5RS rules of how internal
definitions are treated apply to all the definitions in the
module body (both ordinary and syntax), including all code
include
d from files. This is often a source
of errors when moving code from the top-level into a module
because:
All definitions must appear before all expressions,
The list of definitions is translated into
letrec
/letrec-syntax
,
which means it must be possible to evaluate each right-hand
side without assigning or referring to the value of any of
the variables being defined.
This often necessitates re-arranging the code and the
introduction of set!
expressions. Here
is an example of a sequence of top-level
definitions/expressions and how they need to be rewritten so
that they may appear in a module body:
(define (foo) 1) (define bar (foo)) (do-some-stuff) (define (baz) (bar)) ==> (define (foo) 1) (define bar) (define (baz) (bar)) (set! bar (foo)) (do-some-stuff)
The general strategy is to go through the list of expressions/definitions from top to bottom and build two lists - one of definitions and one of expressions - as follows:
If a non-definition is encountered, append it to the expression list
If a "naked" definition (i.e. a definition
whose right-hand side is not a function) that refers to a
binding defined within the module is encountered, append an
empty definition to the definition list and append a
set!
with the right-hand side
expression to the expression list
Otherwise, i.e. for an ordinary definition, append it to the definition list
The concatenation of the resulting definition list with the expression list makes a suitable module body.
Modules are lexically scoped. It is possible to define
modules inside lambda
s and inside other
modules and to export modules from modules. Example:
(define (f c) (module foo (bar) (module bar (baz) (define (baz x y) (- x y)) (display "defining baz\n"))) (if (> c 0) (let ((a 1)) (import foo) (let loop ((b c)) (import bar) (if (> b 0) (loop (baz b a)) (f (- c 1)))))))
The expressions in a module body get executed at the time and
in the context of module definition. So, in the above example,
the body of bar containing the display statement is executed
once for every call to f
rather than once
for every iteration of the inner loop containing the import of
the bar
module.
There are quite a few more things you can do with modules. For instance one can define anonymous modules, which are a short cut for defining a named module and then importing it, import selected bindings from a module and renaming them rather then importing all bindings as is etc etc. For more details again refer to the Chez Scheme user manual.
Libraries provide a means of encapsulating code that can be shared by many, independently developed applications.
Libraries are simply bundles of Scheme code, usually precompiled, which
are packaged so that they may be resolved relative to a library path.
Libraries are typically compiled using the meachanism from the section called “Creating Libraries”.
Loading the resulting library makes the library available
to the loading code. To create a compiled library from a module,
compile a source file which contains any necessary
require-library
calls, followed by the
module definition. When loaded, this will cause the necessary
libraries to be loaded, and then define the module
into the environment. For example, the source file may resemble:
(require-library 'sisc/libs/srfi/srfi-1) (require-library 'com/foo/lib2) (module lib3 (a-function) (import srfi-1) (import com/foo/lib2) (define (a-function) (do-something (another-function))) (define (another-function) (something-else))
Libraries should not depend on any top-level definitions outside the standard SISC top-level, except the definition of other library modules. Otherwise it is not possible to use the libraries portably.
Libraries can be packaged with supporting code (e.g. ordinary Java code and native modules) and other libraries into jar files. A typical structure for such a jar file would be
com/foo/lib1.scc com/foo/lib1/Class1.class com/foo/lib1/Class2.class com/foo/lib2.scc com/foo/lib2/Class1.class com/foo/lib2/Class2.class com/foo/lib3.scc com/foo/lib3/Class1.class com/foo/lib3/Class2.class
It is usually a good idea to name a module after the
path names in the jar, for example
com/foo/lib{1,2,3}
in the above example.
SISC supports SRFI-55 for loading libraries and extensions as well.
SRFI-55 provides require-extension
, which in SISC
simultaneously loads a library, then imports its module definition into the
current interaction environment. This may be more convenient than the
combination of require-library
and import
,
when one is loading dependent top-level libraries for a program.
It is less flexible, though, since you cannot import into a lexical scope.
SRFI-55 is supported in the initial SISC environment, no
require-library
or
import
is needed to use
require-extension
.
At the time of this writing, SISC supports two extension
identifier schemes, the srfi
scheme as
required by SRFI-55 itself, and a SISC specific
lib
scheme for loading a SISC library.
Some examples:
Example 10.1. Loading and importing with require-extension
; Load and import SRFI 1 (require-extension (srfi 1)) ; Load and import SISC library com/foo/lib1 (require-extension (lib com/foo/lib1)) ; Load and import SRFIs 13 and 14, ; and SISC libraries com/foo/lib2 and com/foo/lib3 (require-extension (srfi 13 14) (lib com/foo/lib2 com/foo/lib3))
SISC modules loaded using the lib
extension scheme must use the full path and file as the module
name. For example, in the above example,
com/foo/lib1
's module name must be
com/foo/lib1
.