[R6RS] modules

R. Kent Dybvig dyb
Sun Aug 22 15:51:47 EDT 2004


> Kent> A tool can determine if the requires are necessary for a
> Kent> particular build, but it cannot necessarily determine whether a
> Kent> require can be eliminated from the source code, since a change
> Kent> in an imported syntactic abstraction can affect whether other
> Kent> module imports are used.  For example, does (a x) reference x?
> Kent> We can answer this question for a particular definition of a but
> Kent> not for all possible definitions of a.

> Well, but isn't that an obscure case?  Isn't the more common case that
> indeed no binding for a given import even occurs in the code?  ...

The point is that the "import" may not actually appear in the code, just
a symbol, keyword, or local variable with the same name; the a in (a x)
may expand into quote, or x may be a auxiliary keyword recognized by a,
or x may be bound by some enclosing imported construct.  Maybe a couple of
more elaborate examples would help.  Let's say instead of (a x) we have:

(module ---
  (require (only new-math sin asin))
  (require (only case-tools state-case bind-stuff))
  ---
  (bind-stuff ((x sin) ---)
    (state-case (find-next q)
      ---
      [(sin x) (make-sine-wave x)]
      ---))
  ---)

Is there a reference to the new-math sin in this code?  Not if bind-stuff
creates a local binding for sin or state-case treats it as a keyword or
quoted symbol.

Or maybe we have:

(module ---
  (require (only debug-stuff when-debugging))
  (require (only trace-utils stack-trace))
  ---
         (when-debugging (stack-trace s))
  ---)

Is there a reference to stack-trace?  Not when we've compiled our
application against a version of debug-stuff that has debugging disabled.

In the first example, it would be nice for our tool to tell us we
can eliminate the require for new-math, but in the second, it would
be annoying if not confusing to be told we can eliminate the require
for trace-utils.

> ... How does this fit in with the local-import scenario?

> Maybe I misunderstood you: I thought your point was that programmers
> will just delete the import along with the code that relies on them as
> a matter of course---because the import is part of the code.  But you
> seem to be hinting at a deeper issue.

No, that's essentially it.  Local requires reduce the likelihood of stale
requires simply because they are in closer proximity to the code that
uses them.  If I have a 5,000-line module and need new-math for just one
procedure, I can put the require in that procedure.  If I later discard
the procedure, I automatically discard the require as well without a
second thought.  If I just modify the code so that new-math isn't needed,
it's more likely I'll see the require there and decide whether I need
it or not, and the decision is not likely to be difficult.

The tighter one ties down require forms, the easier program maintenance
becomes.  We can tie them down tighter by specifying the exact set of
imports, e.g., (require (only foo x y)), by renaming or specifying a
prefix, e.g., (require (add-prefix foo foo:)), or by limiting the scope.
Doing two or three of these things might be even better in some cases,
but any one of them reduces the need for the others.  Limiting scope
has additional benefits as well.

If you don't care about stale requires, you might still care about the
imported module and how its imports are used, if you're thinking about
changing the imported module.  It's much easier to determine the effect
of a change if the amount of code you have to scan looking for uses of
your exports is limited.

Incidentally, I use local require for the second example above

  (when-debugging (let () (require trace-utils) (stack-trace) s))

the trace-utils package need be included in my app only when I'm
debugging.  Semantically, because invoking trace-utils may cause side
effects, we may not be able to discard it otherwise.  This is the "reduced
overhead and bloating" benefit of local require that I mentioned in my
earlier note.

> Kent> Here's a simple one:  Suppose I want to make sure I get the r6rs cons
> Kent> and not some other cons, so I write:

> Kent>   ((let () (require scheme) cons) x y)

> Kent> With top-level require only, I would write:

> Kent>   (module ---
> Kent>     (require (add-prefix scheme "real:"))
> Kent>     ---
> Kent>                    (real:cons x y)
> Kent>     ---)

> Kent> I think the former is clearer, but others may not.  (Plus, I don't have
> Kent> to think of a prefix or remember what it was.)

> I think the latter is clearer, in particular if "real" is renamed to
> "r6rs." Moreover, will the former still work if "scheme" has been
> rebound?

Of course not.  Nor will the latter if real:cons is rebound.  Code is
always at the mercy of its enclosing code, as it should be.

> (And somehow, this seems like a fairly contrived example to
> me :-) )

It's a simple example, but similar cases happen often enough in my code.
Perhaps they would in yours if you had a local require form.

> Kent> Not at all.  I'm not sure how I gave that impression, but I would
> Kent> certainly want local imports to shadow local bindings.  Modules create
> Kent> sets of bindings; require forms make them visible.

> I guess I don't understand this paragraph at all:

> Kent>   * Should bindings established by run-time require shadow bindings
> Kent>     that would otherwise be visible within transformers, and visa
> Kent>     versa?  For example, if M exports x, does
> Kent>       (let ([x 3]) (require only-for-syntax M) x)
> Kent>     evaluate to 3 or to an out-of-context error?  (I prefer error;
> Kent>     I'm not sure about Matthew.)

> Could you elaborate?

It's just this kind of thing:

  (let ((cons 3))
    (require only-for-syntax scheme)
    cons)

I prefer that this be an out-of-context error, since I don't like
having two visible bindings, usable or not, for the same identifier in
the same scope.  But one could say that the require-for-syntax doesn't
shadow run-time bindings, just compile-time bindings, so that it should
evaluate to 3.  (The 2-lisp folks, e.g., Common Lispers, would have it
evaluate to 3 in any case, since the binding imported from scheme is a
function, not a "real" value.  But that's a different can of worms.)

Of course, this example is contrived, and perhaps less troublesome than
the case where a require "not for syntax" might affect the output but
not the implementation of a local macro (or local macro within a local
macro, etc.), but I'll leave the development of less contrived and more
troublesome examples up to the reader.

Kent


More information about the R6RS mailing list