[ruby-core:109898] [Ruby master Feature#10320] require into module
From:
"shioyama (Chris Salzberg)" <noreply@...>
Date:
2022-09-15 03:19:23 UTC
List:
ruby-core #109898
Issue #10320 has been updated by shioyama (Chris Salzberg).
The `wrap` option to `load` has recently been expanded to allow passing a module instead of a boolean (https://bugs.ruby-lang.org/issues/6210). This opens the doors to new approaches to this problem.
I think there is a huge opportunity to improve Ruby, and particularly to improve its "namespace hygiene", in the way JS module systems work in Javascript.
Unlike others I've seen implemented in Ruby (including the [modules](https://github.com/lambdabaa/modules) gem, mentioned above), I'd like this to work not only for code that explicitly enables it (with `export`, etc.) but also (and mainly) for code (particularly gem code) that does not.
I've created a gem called [Im](https://github.com/shioyama/im), which depends on a few patches in [this Ruby branch](https://github.com/shioyama/ruby/tree/import_modules) ([c43870](https://github.com/shioyama/ruby/commit/c4387043437b0851306ef199f9417be609c98827) is the main one, plus the bug fix in https://bugs.ruby-lang.org/issues/18960). My goal here is to load _existing gems_, including some of the ones we use often. I want to focus on something concrete which would potentially immediately impact how gems use the global namespace.
With this gem and the patches provided, you can e.g. load `activemodel` and use it in this way:
```ruby
require "im"
extend Im # adds `import` method
mod = import "activemodel"
```
At this point, `ActiveModel` has been loaded but it is not in the global namespace, and can only be found in `mod`:
```ruby
ActiveModel
#=> uninitialized constant ActiveModel (NameError)
mod::ActiveModel
#=> ActiveModel
```
What is very interesting to me is that, _using Ruby's built-in temporary/permanent naming functionality_, we can alias either all of the gem or parts of it.
So for example, naming the module immediately names everything inside of it:
```ruby
MyRails = mod
```
Now we have `MyRails::ActiveModel` (but no `::ActiveModel`).
You can confirm with the example in the gem's readme that this namespaced ActiveModel works (at least for simple stuff, haven't tested more thoroughly yet).
There are a few things I do to make this work:
1. In the Ruby patch, I make the `wrap` argument to `load` apply to further `require`s inside the loaded script. This is actually as simple as removing (actually commenting out) a line.
2. Apply the `top_wrapper` created in `load` to native extensions (specifically, `rb_define_class` & `rb_define_global_const`)
3. Make named modules/classes under an anonymous namespace return their name minus the namespace as their temporary name when called with `name`. This is necessary to use gems like Rails which use `name` to load files, etc.
4. Resolve top-level references (`::Foo`) when loaded with `wrap` to the top of the module namespace, rather than the "absolute" top. For now I've done this in the gem using `const_missing`, but I intend on moving this to the Ruby patch.
With the changes above, the gem is able to setup a registry, patch `Kernel#require`, `Modul#autoload` etc and make this all work (sort of).
To make this all work, I use a lot of aliasing constants from one module namespace to another. This actually seems to work pretty well! So for example, code like this (similar to ActiveSupport):
```ruby
class NilClass
def to_s
# ...
end
end
```
would define a _new_ class under the module namespace. So I alias `mod::NilClass` and other predefined constants to `NilClass`, which then makes this work.
What is amazing to me is _how little is required to make this work_.
Obviously this is very much a proof of concept at this point (and I want to emphasize that). But if you look at the changes to Ruby, they only impact code that uses the `wrap` option to `load`, and there are only a few of them. Also, to me at least, they feel quite natural, since they are simply extending a concept that already exists (`top_wrapper`) to other places where it is not yet applied.
----------------------------------------
Feature #10320: require into module
https://bugs.ruby-lang.org/issues/10320#change-99141
* Author: sowieso (So Wieso)
* Status: Open
* Priority: Normal
----------------------------------------
When requiring a library, global namespace always gets polluted, at least with one module name. So when requiring a gem with many dependencies, at least one constant enters global namespace per dependency, which can easily get out of hand (especially when gems are not enclosed in a module).
Would it be possible to extend require (and load, require_relative) to put all content into a custom module and not into global namespace?
Syntax ideas:
~~~ruby
require 'libfile', into: :Lib # keyword-argument
require 'libfile' in Lib # with keyword, also defining a module Lib at current binding (unless defined? Lib)
require_qualified 'libfile', :Lib
~~~
This would also make including code into libraries much easier, as it is well scoped.
~~~ruby
module MyGem
require 'needed' in Need
def do_something
Need::important.process!
end
end
# library user is never concerned over needed's content
~~~
Some problems to discuss:
* requiring into two different modules means loading the file twice?
* monkeypatching libraries should only affect the module → auto refinements?
* maybe also allow a binding as argument, not only a module?
* privately require, so that required constants and methods are not accessible from the outside of a module (seems to difficult)
* what about $global constants, read them from global scope but copy-write them only to local scope?
Similar issue:
https://bugs.ruby-lang.org/issues/5643
--
https://bugs.ruby-lang.org/
Unsubscribe: <mailto:ruby-core-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>