>> The default environment is irrelevant to most Scheme libraries, since
> it is irrelevant to any library defining modules with
> ‘define-module’, ‘define-library’ or ‘library’ forms (which is pretty
> much the only reasonable way to define modules unless you are making
> your own system). This already reduces a lot of compatibility
> concerns. An important exception is anyone doing ‘eval’ or compiling
> expressions (think anyone doing the equivalent of “guile -l
> script.scm”.
>
>Unfortunately, the "default environment" in the sense of "core bindings"
definitely is relevant to libraries using `define-module`. For example,
in this module:
```
(define-module (foo)
#:export (foo))
(define foo
(cons 'a 1))
```
at least the bindings for `define`, `cons`, and `quote` are implicitly
imported from `(guile)`.
Yes, that’s why #:pure? #true (IIRC the right keyword argument) should be used.
(+ deprecation warning when #:pure? is absent?)
>R6RS libraries don't have this problem. If instead you write:
```
#!r6rs
(library (foo)
(export foo)
(import (rnrs base))
(define foo
(cons 'a 1)))
```
Here you are demonstrating how R6RS libraries have the _same_ problem. You
should at least have included a version number next to (rnrs base). Who knows,
maybe R8RS will rename ‘cons’ to ‘make-pair’?
As written, it is implicit which version of (rnrs base) is imported (*) – is it
R6RS? Is it R7RS (no, because R7RS uses (scheme ...), but it _could_ have used
(rnrs ...) instead)? A hypothetical future R8RS? (There might be (?) a rule
that if no version is mentioned, the latest version available is used, but
that’s its own source of incompatibilities ...)
(*) no #!r6rs does not count – that’s good for lexical syntax, but the version
number of the _modules_ go into the (import ...). AFAICT, nowhere is the
version number of the (rnrs ...) treated specially w.r.t. #!r6rs.
Compare this with how (implicitly) (guile) is imported – Guile doesn’t know
_which_ version of the (guile) API should be used because it isn’t told (and
currently, there is no option to tell Guile).
That a module is implicitly imported isn’t really a problem, what is the
problem is that the _version_ (in terms of API, not implementation) is implicit.
> the bindings for `define`, `cons`, and `quote` are explicitly imported
from `(rnrs base)`. If the `import` clause were empty, you would get
unbound identifier errors. (The report specifies the meaning of
`library` and its `export` and `import` sub-forms, but it also specifies
that they are not bound by any of the libraries specified by the report,
though `library` is often implemented as a binding in some sort of
implementation-specific start-up environment that is not in scope inside
a library.)
>Similarly, in the Racket module:
```
(module foo racket/base
(provide foo)
(define foo
(cons 'a 1)))
```
>the module's language, `racket/base`, is explicitly the source of the
binding for `provide` as well as those for `define`, `cons`, and
`quote`. If you replaced `racket/base` with `typed/racket/base` or
`lazy`, you would get a valid module with different meanings for those
bindings.
I could be wrong since I don’t the well how Racket is developed, but barring
evidence to the contrary, I’d assume that the bindings in racket/base vary
depending on the version of Racket – just like Guile’s default environment.
Assuming this is true, then Racket has (in terms of implicitness) pretty much
(not exactly, but _pretty much_) the same problem as Guile.
>Of course, you could avoid some indentation by writing the above as:
```
#lang racket/base
(provide foo)
(define foo
(cons 'a 1))
```
Unless the API of racket/base never changes over time: same problem, no version
number or equivalent is included.
>Andy Wingo's "lessons learned from guile, the ancient & spry"
(<https://wingolog.org/archives/2020/02/07/lessons-learned-from-guile-the-ancient-spry>)
concludes in part:
> But as far as next steps in language evolution, I think in the short
> term they are essentially to further enable change while further
> sedimenting good practices into Guile. On the change side, we need
> parallel installability for entire languages. Racket did a great job
> facilitating this with #lang and we should just adopt that.
>
>I agree.
>
> Obviously `#lang` does much more than this (we Racketeers tend to say
`#lang` as shorthand for a bunch of complementary but mostly independent
features), but I think a particularly important aspect is that each
module should explicitly specify its "language"/"default
environment"/"core bindings". If done well, this actually *avoids* the
compatibility concerns some have raised: when you want to make a
breaking change, you pick a new name, and things using the old name keep
working, interoperably.
The thing is, AFAICT using #lang does not imply “explicitly specifying the core
bindings”, as mentioned previously – AFAIK Racket does not pick a new name
every time the list of bindings is changed in some way.
There are benefits to have a standard way to indicate the language of a file
(e.g. #lang) (and I guess if you want to you could define a bunch of fake
languages that also include a couple of imported modules(and which these are
depend on the precise fake language), though I don’t see why not just import
those as modules), but that doesn’t mean we should repeat Racket’s mistakes
with what else (and how) it also uses #lang for.
Best regards,
Maxime Devos