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
> <da...@davidchristiansen.dk> 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 racket-users+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to