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.