racket / rhombus

Rhombus programming language
Other
332 stars 59 forks source link

Make an RFC about modules, units, linklets, and linking #75

Open jeapostrophe opened 5 years ago

jeapostrophe commented 5 years ago

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

sorawee commented 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)

rocketnia commented 5 years ago

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.

rocketnia commented 3 years ago

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:

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?

benknoble commented 3 years ago

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.

mfelleisen commented 3 years ago

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.)

rocketnia commented 3 years ago

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.

mfelleisen commented 3 years ago

That's not completely correct; see GPCE 2005 but yes, it's definitely a short-coming of units.