scheme48-module-system


A users guide

Scheme48 comes with a powerful and interesting module system. We try to explain how to use it in simple words here.

Modules and Structures

Module systems are difficult, because there are so many of them out there. The terminology is not unified, so before starting out on any discussion of a module system, we have to settle for common definitions of terms. Scheme48 defines the following terms for its module system. If you don't know quite yet what any of the descriptions mean exactly, ignore it. Come back here later and see whether it is clearer now.

Package

A package is an environment -- that is, a set of variable/value bindings. You can evaluate Scheme forms inside a package, or load a file into a package. Packages export sets of bindings; these sets are called structures.

Structure

A structure is a named view on a package -- a set of bindings. Other packages can open the structure, importing its bindings into their environment. Packages can provide more than one structure, revealing different portions of the package's environment.

Interface

An interface is the ``type'' of a structure. An interface is the set of names exported by a structure. These names can also be marked with other static information (e.g., advisory type declarations, or syntax information).

We use the term structure from now on.

A configuration language

What happens if we - somehow - request the contents of structure Foo be made available?

The system has to find out ...

  1. ... which structures Foo requires, and load them into a fresh package.
  2. ... which file(s) are required for Foo, and load them into that package.
  3. ... which bindings are exported from Foo, and export them to the requesting structure.

If the package is already available, of course we can skip steps 1 and 2.

These connections between a structure name, the dependencies, files and exports are described in Scheme48 using a configuration language.

That is, Scheme48 defines a language (with a syntax close to Scheme) which defines all those relations.

A minimal example

So your program consists of two kinds of files: The source files and separate configuration language files. The separate file, often called packages.scm (which is often accompanied by an interfaces.scm, but more on that later), contains the configuration description of our program. What structures exist, what do they depend on, what do they export, which files are used. All concisely together in the configuration description.

Let's look at an example. We want to write a GREETINGS structure that exports a HELLO procedure which greets a number of people.

The source file is easy:

 ;;; hello.scm -- A greetings structure 
 (define (hello . whos) 
  
   (display "Hello") 
   (for-each (lambda (who) 
               (display ", ") 
               (display who)) 
             whos) 
   (display ".") 
   (newline)) 

We now know that we have to tell Scheme48 about a new structure using the configuration language, preferably in a separate file:

 ;;; packages.scm -- A configuration 
 (define-structure greetings (export hello) 
   (open scheme) 
   (files hello)) 

This looks interesting. DEFINE-STRUCTURE is the main form in the configuration language and defines a structure. The first argument is the structure name. The second argument is an expression which defines what this structure exports, with the (export hello) form being the simplest of them - we'll learn to know others later on. Then follows a list of OPEN clauses - these tell Scheme48 which structures we need. The scheme structure is almost always required: It defines the basic scheme language, include such things as DEFINE. The last clause, FILES, tells Scheme48 which files it should load for this structure. It automatically appends an ".scm" extension.

That's it!

How to use it

To use a structure, the structure has to be defined using the configuration language. This is a two-step process which can be confusing at first: You first load a file with configuration descriptions, but that file defines only your structures - to use any of them, you have to open them.

In Scheme48, you can do the following:

 > ,config ,load packages.scm    ; Load the configuration file 
 > ,open greetings 
 > (hello "Jonathan" "Richard") 
 Hello, Jonathan, Richard. 
 #{Unspecific} 

Ok, what have we been doing here? First of all, we told the system to load our packages.scm into the configuration module using ,config ,load. This tells the system which structures we define. To open the structure and import its exports, we have to use ,open. That's it.

Considering Scheme48's normal operation - load everything you need, dump a heap image, and use that in the future - this works fine for us.

SCSH

The Scheme Shell is more tailored towards traditional UNIX shells, hence we don't want to load configuration files into the image from the REPL. Therefore, scsh defines a few command line arguments which load configuration files and opens structures:

-o  structure         Open structure in current package.
-lm module-file-name  Load module into config package.

So, for SCSH, the following works in our example:

scsh -lm packages.scm -o greetings -c '(hello "Olin" "Brian" "Martin" "Mike")'

Interfaces

Earlier, we promised to talk more about interfaces. It's actually quite common for library structures to have long export lists (after all, that's what a library is for: export stuff). Not only can those clutter up the configuration file, they could also be used multiple times. Maybe we have two structures which provide the same interface? Since interfaces are a conceptually separate entity from the structures, the configuration language allows us to define interfaces separately:

 (define-interface greetings-interface 
   (export hello 
           hej 
           hola 
           hallo)) 

We can use that name instead of the EXPORT clause in DEFINE-STRUCTURE:

 (define-structure greetings greetings-interface 
   (open scheme) 
   (files hello 
          hello-helper)) 

(Don't be confused about the additions of more hello procedures or the new file there - that means exactly what you think it does).

Common Practice

It is common practice with Scheme48 programs to ship with two files besides the normal source files: interfaces.scm and packages.scm. To use a program, the user is expected to first load the interfaces.scm and then the packages.scm into his scheme48 system. After that, he can open a structure of the program and run procedures from there, or build a new heap image.

The separation of interface and package definitions is pretty useful. Not only does it help to keep some overview over the structures, but also find out easily which structure export a given binding.

A Glimpse of Power

The configuration language allows all kinds of nifty tricks. Let's take a short look at some of them:

Subset, With-Prefix and Modify

When you open a structure, you import all bindings from that structure into your package. This might not always be what you want. The first question that arises is "Can I somehow get fewer bindings?". Sure:

 (define-structure espanol espanol-interface 
   (open scheme 
         (subset greetings (hola))) 
   (files espanol)) 

This only imports the HOLA binding.

Oh, maybe the package exports lots of interesting procedures, but they would pollute your namespace too much:

 (define-structure greeter greeter-interface 
   (open scheme 
         (with-prefix greetings greet/)) 
   (files greeter)) 

This provides GREET/HELLO, GREET/HEJ, GREET/HOLA and GREET/HALLO in your package.

Of course, maybe you want to import only one binding, but that with a special, new name:

 (define-structure espanol espanol-interface 
   (open scheme 
         (modify (subset greetings (hola)) 
                 (rename (hola greet-in-espanol)))) 
   (files espanol)) 

This would provide only the binding HOLA from the GREETINGS structure, named GREET-IN-ESPANOL in your package.

The MODIFY clause also supports HIDE, EXPOSE, RENAME and ALIAS in addition to the PREFIX - explore a bit to find out what they do.

Compound Interface

If we want to define a structure which adds a single greeting procedure, but else export the same stuff as the GREETINGS structure, we can do the following:

 (define-interface greetings-eo-interface 
   (compound-interface greetings-interface 
                       (export saluton))) 
  
 (define-structure greetings-eo greetings-eo-interface 
   (open scheme 
         greetings) 
   (files hello-eo)) 

Using COMPOUND-INTERFACE, it is easy to combine different interfaces.


category-implementations