I ended up spending the majority of my weekend working on this, and even
after two days, it still doesn’t work quite right. My verdict is that
it seems impossible to do perfectly seamlessly, but it seems possible
(but maybe hard) to get 85% there. Here’s a (rather long) overview of
what I tried and the problems I encountered — some people might have
some suggestions.
I implemented two namespaces, and each namespace corresponds to a
(use-site) syntax introducer. The introducers are stored in syntax
parameters that are parameterized by #%module-begin. I implemented a
~type pattern-expander that uses the type introducer to attach the type
scope to a piece of syntax before parsing it, which provides a nice,
declarative way to annotate which pieces of forms belong in which
namespace. This part actually works great — for single-module programs,
values and types can cleanly use the same symbolic name without problems
— but the devil is in the details.
I ran into two main problems when implementing this, neither of which I
have come up with completely satisfactory solutions to.
1. Importing and exporting types is complicated.
My instinct is that Matthew’s suggestion of using submodules for
namespace management is a good one, but as Alex pointed out, I’m
not sure how to actually implement it in practice. I tried the
simpler name mangling scheme, and I managed to get it mostly
working. Exporting types is done with an explicit `type-out` form
(though most of the time it isn’t necessary, since `type-out` is
implicit when providing types with `data` or `class`), which
applies the type scope and mangles the names by prepending
`#%hackett-type:` to the beginning. Similarly, on the importing
side, Hackett actually provides a modified version of `require`
that wraps the entire set of import specs with a require
transformer that unmangles the names and injects them into the
proper scope.
This works okay, but it has some problems:
a. Forms like rename-in, only-in, and except-in don’t work on
types. I think this is largely unavoidable, since they
fundamentally don’t understand that there could be multiple
bindings with the same name, so the solution is to reimplement
rename-in, only-in, and except-in with versions that allow
some sort of annotation that a binding being controlled should
operate on type bindings. For example, a user could write
something like the following:
(rename-in hackett [(type String) Text])
This is a little bit of unfortunate extra work, but it doesn’t
seem fundamentally difficult.
b. The bigger problem is that this scheme doesn’t work at all for
types provided by the module language.
What do I mean by that? Well, note that when a user authors a
module with `#lang hackett` at the top, the reader eventually
produces a module form like the following:
(module mod-name hackett
....)
Types that are built-in to Hackett are provided by that module
language specification. This is a problem, because they won’t
be properly unmangled since module languages do not allow any
customization of how bindings are introduced (unlike
`require`). This is a real problem, and there doesn’t seem to
be any good solution available.
The only real option appears to be having the reader insert a
`require` in the resulting program, so the resulting module
looks like this:
(module mod-name hackett
(require hackett)
....)
However, this doesn’t really work, because while the bindings
*are* properly introduced, they are now introduced by a
`require`, not the module language. This distinction seems
irrelevant, but it isn’t — bindings brought into scope by
`require` cannot conflict with other bindings from other
`require`s. This means that it is now impossible for users
to shadow names from `#lang hackett` with their own bindings.
In other languages, this might be okay, but in Racket, it
isn’t acceptable.
My current compromise is to at least mitigate the damage by
only making the inserted `require` import types, so core
forms and functions can still be shadowed by other imports,
but types cannot. This is more manageable, but it’s still
unpleasant and confusing when it causes problems.
Furthermore, making this change in the reader causes serious
issues when using `hackett` as a module language explicitly,
not as a `#lang`. This is not common when writing most
modules, since modern Racket style is to always use `#lang`,
but module languages still crop up when defining submodules.
This means a user defining a submodule with `hackett` as the
module language with have to manually insert the `require`,
since there is no reader control to introduce it implicitly.
This is a wart I have not yet been able to resolve.
2. Related to the very last point of the previous issue, submodules
seem to generally make things hard, or at least module* submodules
with #f for the module path do.
The documentation seems a little unclear on the details of how the
expansion model works for module* submodules with #f for the module
path. Ultimately, though, they inherit bindings from their parent
modules, so users expect to be able to access both types and
bindings in the parent module’s scope. This means that these
submodules should NOT have fresh value and type namespace scopes!
They must inherit them from their parent modules.
As far as I can tell, this should be possible, but I have been
having a very hard time giving a nested `#%module-begin` access to
its parent module’s syntax parameters (defined using
`splicing-syntax-parameterize`). I’m still working on this, and I’m
thinking it might be solvable using first-class definition
contexts, but I find much of the implementation of
`splicing-syntax-parameterize` in `racket/splicing` highly
confusing.
Additionally, I found that the way `module+` lifts module
declarations to be seemingly incompatible with
`splicing-syntax-parameterize`, since it would lift them outside
of the end of the parameterized block. To solve this, I came up
with my own version of `module+` that cooperates with Hackett’s
`#%module-begin`. That seems to work, but again, it’s unfortunate
that the extra work is required.
Either way, this is critical to making this workable in Hackett,
since Hackett uses `main` and `test` submodules for the same
reasons Racket does.
So that’s the state of things. Apologies for the very long email, but
this issue has ended up being much more nuanced than I had hoped! I feel
I may be missing something that would simplify things significantly,
which would be nice, but I’m not sure what that would be. Any
alternative suggestions would be welcome.
> On Oct 15, 2017, at 4:32 PM, David Christiansen
> <[email protected]> wrote:
>
> The LCF-style tactic engine that Sam and I have running in the macro
> expander uses a transformer binding to _invoke_ the tactic engine, but
> all of the individual tactics live in phase 1 bindings. Though there's
> no call to syntax-local-eval - they're just used directly.
>
> I would think that, similarly, type ascription could be a macro that
> associates entirely phase-1 type values with run-time expressions. But
> there's surely aspects of this that I don't see :-)
I think the reason I can’t use this is that (as Matthew pointed out to
me on slack), while types and values are in totally separate namespaces
in Hackett, users still expect to be able to bind types locally (think
ScopedTypeVariables in GHC) and have those scopes correspond to phase 0
binding regions. Things like `let-syntax` and internal definition
contexts make this possible using phase 0 transformer bindings, but
phase 1 and phase 0 bindings are too orthogonal for this to be an
option.
Alexis
--
You received this message because you are subscribed to the Google Groups
"Racket Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
For more options, visit https://groups.google.com/d/optout.