Chapter 10.  Modules and Libraries

Table of Contents

Modules
Overview
Style
Modularizing Existing Code
Evaluation
Libraries
require-extension

Modules

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.

Overview

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 defined or define-syntaxed in the body of the module, or imported from another module.

Style

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).

Modularizing Existing Code

Since module bodies are treated like the bodies of lambdas, 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 included 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.

Evaluation

Modules are lexically scoped. It is possible to define modules inside lambdas 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

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.

require-extension

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.