Open jeapostrophe opened 5 years ago
Here's something I want to be able to do:
Say, fancy-app
is a package that turns (f _ 1 _)
to (lambda (x y) (#%app f x 1 y))
. reverse-app
is a package that turns (f a b c)
to (#%app f c b a)
.
To use fancy-app
or reverse-app
, I would write: (require (fancy-app racket))
or (require (reverse-app racket))
respectively. That is, fancy-app
and reverse-app
are module functors.
If I want my #%app
to have both functionalities, I could either write: (require (fancy-app (reverse-app racket)))
or (require (reverse-app (fancy-app racket)))
.
One would transform (f _ _ 1)
to (lambda (x y) (f 1 x y))
. Another would transform (f _ _ 1)
to (lambda (x y) (f 1 y x))
.
(thanks to @AlexKnauth for the discussion)
One way to handle that scenario within the current module system would be for the fancy-app
and reverse-app
libraries to export macro-defining macros:
(require (only-in racket/base [#%app base-app]))
(require (only-in fancy-app define-fancy-app))
(require (only-in reverse-app define-reverse-app))
(define-fancy-app my-fancy-app base-app)
(define-reverse-app #%app my-fancy-app)
; ... code that uses the #%app we just defined ...
Similarly, I think a lot of use cases for ML-style functors are attainable using submodule-defining macros.
These past few months, I've been thinking about approaches to parameterized modules. While a module-generating macro can work in a pinch, it means we're compiling the same module lots of times, and none of their types are compatible. It would be much better to be able to cache compilation results and run-time module instantiations, just like Racket already does for non-parameterized modules.
But where do we store the compilation results for each invocation of a module? I can imagine several possible answers:
#lang
, but maybe there's a way.namespace-attach
to attach the appropriate variant of module A to a "compilation registry" and then compile new versions of {M1, M2, ..., Mn} which draw upon that registry.#f
from equal?
isn't a noisy error, and since the person observing the bug might be a complete bystander who's merely upgraded some packages, this could be a difficult bug to diagnose.find-relevant-directories
to find all the call sites.find-relevant-directories
approach has problems (see above).I've done a lot of thinking these past few months about the Designated Entrypoints and Call Sites options. The Call Sites option has been interesting to think about in a lot of ways, and this week I was writing up a proposal based on it, but I'm starting to see its dependency hell as a rather important point against it.
At this point, the Dedicated Location option is the only one I find fully compelling, even if new compiler features turn out to be needed for that one.
What do you all think of the tradeoffs here?
I wrote a fair bit of ML before switching to Racket, and (while I haven't yet programmed with them) it seems units with import lists are the equivalent of ML functors? Perhaps I'm missing something… or perhaps the original proposal wants to coalesce some features of both modules and units? It's not entirely clear to me.
Units were directly inspired by ML functors and weaknesses we experienced with those. At the time, ML (spec, implementation) did not allow recursion and dynamic loading of functors, nor could functors accept functors. (The inspiration was a particularly kind of extensible language interpretation. It's now known as algebraic-effect interpretation.)
perhaps the original proposal wants to coalesce some features of both modules and units?
Yeah, Racket module imports retrieve compile-time entities (like macros) and run-time entities (like functions). Units, as they exist now at least, aren't a full solution for abstracting over Racket module imports because they can only handle the run-time parts.
That's not completely correct; see GPCE 2005 but yes, it's definitely a short-coming of units.
Racket1 has two user-facing methods of modularity: modules and units. Modules are purely compile-time things and always refer to their imports by name, so they are inherently not parameterized. Units are run-time things and support a lot of functionality and power via this nature, such as mutual recursion. Units are implemented purely as macros and work hard to be partially separably compilable (via, for example, macros only inside of unit signatures and not the units themselves.) At the VM level, Racket also supports linklets, which are like a limited kind of unit. Modules are implemented via compilation to sets of linklets.
I would like a modularity mechanism more similar to ML's module system where a module can be abstracted over its imports. I think it is valuable for thinking about programs and performance for this to be a compile-time decision, but I think it is interesting to think about whether the same specification mechanisms could be used for run-time and compile-time situations.
I think there are big risks for making the linking language too complicated to use for small things (which is what I perceive as a major issue with units) and forcing too much tooling outside of the program (which I perceive as a big issue in ML.) I think we can deal with the first by being less ambitious in terms of power and separate-compilability and I think the second is solved by a DSL that is like
require
/provide
.As a tiny concrete thing, I think it would be extremely useful for parameterized modules (functors in ML) to specify a default instance of their imports that is used when the module is run itself and when no linking specification is used.
I think this requires a connection to #9