[R6RS] modules?

R. Kent Dybvig dyb
Mon Apr 19 22:26:55 EDT 2004

I don't object in principle to focusing initially on top-level modules as
long as we don't do something that rules out local modules.  That said,
I want to make two points:

  1. It was a mistake for us not to include internal define-syntax 
     in r5rs.  It may be a similar mistake not to include internal
     modules now.

  2. Even if we don't include internal modules, we should include
     internal imports/requires, which are orthogonal to local modules.

In response to an earlier question, here are some common uses of internal

  * creating one compilation unit from several modules:

    (let ()
      (module A ---)
      (module B ---)

    these modules are often include'd from files

  * isolating the scope of local bindings

    (let ()
      (module (foo) ; often an anonymous module
        (define helper 1)
        (define helper 2)
        (define foo ---))

     now if we decide to modify any of foo's helpers, we can do so
     without looking around for other uses.  We can also delete the
     helpers if we decide to delete foo, without any thought.

  * mixing initialization expressions with definitions

    (let ()
      (module () init-expr ...)

    (Initialization expressions are always performed last, after all
    definitions within a contour.)

As Matthew pointed out, local modules are also useful when writing macros,
especially those that must produce initialization expressions as well
as definitions.

Local imports are even more useful.  We use these most often to gain
access to things in other modules within a limited scope, like grabbing
for something in Chez Scheme's system module that one would ordinarily
not want to have in scope,

    (let () (import system) ($object-ref 'integer-32 x 17))

or redefining a syntactic abstraction in terms of its built-in counterpart

    (define-syntax if ; don't allow one-armed if
      (let ()
        (import scheme)
        (syntax-rules ()
          [(_ e1 e2 e3) (if e1 e2 e3)])))

Of course, local modules aren't much good without local imports.
(Even an anonymous module expands into a named module with an
implicit import form.)

While it's true that internal modules don't work quite the same as
top-level modules, this is really just a reflection of the top level and
the concessions we make with it for interactive programming.  We view
top level definitions as approximating internal definitions rather than
the other way around.

In any case, I don't see either internal modules or internal imports as
being a big challenge.  I see as a bigger challenge coming to agreement
on the semantics of import/require and when the forms in a module body
are evaluated.  I don't see a significant benefit to Matthew's model for
import/require and prefer our simpler notion in which import/require
merely makes visible a set of existing bindings.  I realize that
Matthew's model may help programmers avoid certain kinds of trouble, but
I believe that the additional conceptual overhead outweighs the benefit.
I'm guessing, unfortunately, that Matthew disagrees.

There are a couple of other issues that may or may not be sticking points.

First, Chez Scheme allows imported variables to be assigned.  From brief
experimentation, it appears that MzScheme does not.  Again, our model is
that of making visible existing bindings.  The issue of making variable
bindings available for reading only is an orthogonal issue that can be
easily addressed with identifier macros or some other mechanism.

Second, Chez Scheme requires that hidden exports be explicitly associated
with the corresponding export, e.g.:

  (module A ((alpha foo))
    (define foo ---)
    (define-syntax alpha
      (syntax-rules ()
        [(_ e) (foo e)])))

The reason for this is that, with syntax-case macros, it is impossible
to tell by inspection which identifiers are hidden exports.  For example,
one can write:

  (module B ((alpha foo bar))
    (define foo "fooval")
    (define bar "barval")
    (define baz "bazval")
    (define-syntax alpha
      (lambda (x)
        (syntax-case x ()
          [(_ x)
           (datum->syntax-object #'B (syntax-object->datum #'x))]))))
  (import B)
  (alpha foo) -> "fooval"
  (alpha bar) -> "barval"
  (alpha baz) -> error

Without explicit listing of hidden exports, all identifiers must be made
available.  (We might be able to restrict the use of datum->syntax-object
for this apparently nefarious purpose, but there are legitimate uses for
this technique.)  If all identifiers are made available, there's not
much security, since a macro writer can unintentionally allow fishing
for hidden exports:

  (module C ((alpha foo bar))
    (define foo "fooval")
    (define bar "barval")
    (define baz "bazval")
    (define-syntax alpha
      (lambda (x)
        (syntax-case x ()
          [(_ f) #'(f foo)]))))
        [(_ x) (datum->syntax-object #'x 'bar)])))
  (define-syntax fish
    (lambda (x)
      (syntax-case x ()
        [(_ x) (datum->syntax-object #'x 'bar)])))
  (import C)
  (alpha fish) -> "barval"

Explicitly listing hidden exports limits the damage.  In our
implementation, anything that is not exported, hidden or otherwise, is
simply not available.  Furthermore, unexported variable bindings are bound
with letrec and therefore completely local to the module initialization
and export forms.  This increases both security and efficiency.


More information about the R6RS mailing list