rhombus-prototype icon indicating copy to clipboard operation
rhombus-prototype copied to clipboard

Make an RFC about module top-level effects

Open jeapostrophe opened this issue 6 years ago • 6 comments

Module top-levels can have arbitrary effects. This makes it so that tree-shakers that eliminate the number of requires and definitions in a program can be very ineffective. For example, mutation is used to initialize object% so programs that are written in #lang racket must actually execute class system code even if they don't use it.

An alternative is to require top-levels to be effect-free but build in a notion of an initializer for a set of bindings where if the binding is going to be touched, the initialization code must be run. This would be like a promise that is run for effect.

jeapostrophe avatar Jul 26 '19 15:07 jeapostrophe

This can be used to make (all?) modules cross-phase persistent?

Somewhat related:

  • "Cross-phase persistent gensym?" https://github.com/racket/racket/issues/2760

  • "Make set and all build-in objects cross-phase persistent" https://github.com/racket/racket2-rfcs/issues/93

I think the other common use is to populate a hash with some set of predefined common values.

gus-massa avatar Jul 26 '19 17:07 gus-massa

Why not add a special form, similar to begin-encourage-inline, that libraries can use to mark top-level expressions (and definitions?) that can be omitted under certain conditions, and adjust the relevant tools to cooperate with it?

Making a fundamental change the notion of a module body seems unnecessarily disruptive.

rmculpepper avatar Jul 27 '19 17:07 rmculpepper

The special form you suggest would be the not-default, so I assume that no one would use it. It would definitely give us a tool to go search for ways to improve things, but it makes the behavior I want hard to get rather than always present.

jeapostrophe avatar Jul 28 '19 01:07 jeapostrophe

Could we expose this information in require and provide somehow? Like if I have a module that I require because I need the side effects of instantiating that to run, and I don't need any actual bindings from it, then writing just (require mod) reads strangely and confuses Check Syntax when I mouse over it. I'd rather write something like (require (for-side-effects mod)) to make it clearer that I'm explicitly depending on the side effects of instantiating mod.

On the other side of the module boundary, maybe some sort of (provide (side-effect-out ...)) form could make sense. That encourages module authors to think of executing instantiation side effects as part of a module's public API.

jackfirth avatar Jul 28 '19 04:07 jackfirth

(re the special form proposal) It may be that no one uses it, until they get a bug report or pull request by someone who cares about shaking that code out of their tree. To me, that seems far better than imposing an extra layer of bureaucracy on every module. (Or perhaps it would be possible to add annotations on imports too, not just definitions.) It's also something that you could probably prototype now.

Another issue: Is (define truth (const #t)) allowed in the side-effect-free zone? If so, how do you know it's okay?

rmculpepper avatar Jul 28 '19 10:07 rmculpepper

The way imagined it working is that a module's body must be (#%module-begin (define-values (x ...) e) ...) and that to have a top-level effect, you basically have to write (define eff1 (println "Hello")) and that this effect would happen the first time eff1 is referred to. So if (define f (begin eff1 (lambda (x) (+ x 1)))) and (define g (begin eff1 (lambda (x) (+ x 2)))) were in the module, then if a client module had (f 1) and (g 1), then the println would happen once. I'm not imagining any sort of effect-analysis.

jeapostrophe avatar Jul 28 '19 12:07 jeapostrophe