From: "rubyFeedback (robert heiler) via ruby-core" Date: 2023-02-17T08:35:49+00:00 Subject: [ruby-core:112466] [Ruby master Feature#19024] Proposal: Import Modules Issue #19024 has been updated by rubyFeedback (robert heiler). Personally I think it may be better to leave require(), load() and require_relative() unchanged as-is, and instead add a new, more sophisticated way for ruby to handle loading of stuff in general. That one should then ideally be as flexible as possible, and "extensible" for the future. It could then also allow all of our needs and wants. Many years ago, for instance, I wanted to be able to load up a specific module, but instantly, upon load-time, assign it a new different name. We can kind of simulate that way e. g. require 'foobar' Barfoo = Foobar # and then setting Foobar to nil, I suppose, or # something like that But I wanted that on the require/load situation. We could also integrate autoload-like behaviour into it. And so on and so forth. That could also mean to load ruby code "outside" of any "namespace". Such as "anonymous loading" not polluting the namespace. I am not sure we should add "import" as such, though. People will ask when to use include, and extend, and then import. So I kind of agree with fxn that we should leave require and load as it is, and instead think about more flexible loading/requiring of code as-is. Another feature I wanted to have is that we can assign "authors" to a namespace - not in a way to deny people from using them, but simply to be able to track who made modifications where and when. In some ways this is similar to how refinements can be thought of - we can think of them as "isolated namespace changes" but allowing us more control over the code changes (if we ignore the syntax; I always found the syntax odd for refinements). Perhaps we should create a more unified proposal eventually that can unify all the different ideas, pros and cons, for matz to think about what the best approach would be. One thing austin wrote: > I don���t see any value in namespacing beyond what Ruby has > through modules and classes. I certainly don���t see any > value in the ability to load more than one version of a > piece of code at a time under a different namespace (this > is, IMO, one of the worst parts of JavaScript). Well, refinements already does that to some extent; and I can see the potential value in knowing who made modifications where and when. Right now we all have one unified namespace. This can lead to problems sometimes. I agree it is not a huge problem per se, but when I write: class Colors And some other package is: module Colors and I already included that, then there are some problems. (Or, both use class, or module, and then I may overwrite some toplevel method or a similar problem.) It's not a huge problem per se, mind you, but in these cases, being able to "tap" into a class or module and see where changes were made, when, by whom, and in which package/file, can be useful in my opinion. That is not to say I agree with the proposal here per se, but I wanted to comment on whether there may be use cases - and I think there are. I don't know of a good syntax-way to tap into any of that though. I just think we should be open about different ways how to load up code in ruby, because there are definitely use cases that are not fully covered by require and load. (I use require() about 98% of the time, and I avoid autoload, because I found that the cognitive load of having to remember how to use it offsets the benefits it brings; I also don't need require_relative. I do sometimes need to use load(), e. g. dynamic reloading of code if you need it, but I can see different use cases most assuredly.) ---------------------------------------- Feature #19024: Proposal: Import Modules https://bugs.ruby-lang.org/issues/19024#change-101911 * Author: shioyama (Chris Salzberg) * Status: Open * Priority: Normal ---------------------------------------- There is no general way in Ruby to load code outside of the globally-shared namespace. This makes it hard to isolate components of an application from each other and from the application itself, leading to complicated relationships that can become intractable as applications grow in size. The growing popularity of a gem like [Packwerk](https://github.com/shopify/packwerk), which provides a new concept of "package" to enforce boundaries statically in CI, is evidence that this is a real problem. But introducing a new packaging concept and CI step is at best only a partial solution, with downsides: it adds complexity and cognitive overhead that wouldn't be necessary if Ruby provided better packaging itself (as Matz has suggested [it should](https://youtu.be/Dp12a3KGNFw?t=2956)). There is _one_ limited way in Ruby currently to load code without polluting the global namespace: `load` with the `wrap` parameter, which as of https://bugs.ruby-lang.org/issues/6210 can now be a module. However, this option does not apply transitively to `require` calls within the loaded file, so its usefulness is limited. My proposal here is to enable module imports by doing the following: 1. apply the `wrap` module namespace transitively to `require`s inside the loaded code, including native extensions (or provide a new flag or method that would do this), 2. make the `wrap` module the toplevel context for code loaded under it, so `::Foo` resolves to `::Foo` in loaded code (or, again, provide a new flag or method that would do this). _Also make this apply when code under the wrapper module is called outside of the load process (when `top_wrapper` is no longer set) — this may be quite hard to do_. 3. resolve `name` on anonymous modules under the wrapped module to their names without the top wrapper module, so `::Foo.name` evaluates to `"Foo"`. There may be other ways to handle this problem, but a gem like Rails uses `name` to resolve filenames and fails when anonymous modules return something like `#::ActiveRecord` instead of just `ActiveRecord`. I have roughly implemented these three things in [this patch](https://github.com/ruby/ruby/compare/master...shioyama:ruby:import_modules). This implementation is incomplete (it does not cover the last highlighted part of 2) but provides enough of a basis to implement an `import` method, which I have done in a gem called [Im](https://github.com/shioyama/im). Im provides an `import` method which can be used to import gem code under a namespace: ```ruby require "im" extend Im active_model = import "active_model" #=> <#Im::Import root: active_model> ActiveModel #=> NameError active_model::ActiveModel #=> ActiveModel active_record = import "active_record" #=> <#Im::Import root: active_record> # Constants defined in the same file under different imports point to the same objects active_record::ActiveModel == active_model::ActiveModel #=> true ``` With the constants all loaded under an anonymous namespace, any code importing the gem can name constants however it likes: ```ruby class Post < active_record::ActiveRecord::Base end AR = active_record::ActiveRecord Post.superclass #=> AR::Base ``` Note that this enables the importer to completely determine the naming for every constant it imports. So gems can opt to hide their dependencies by "anchoring" them inside their own namespace, like this: ```ruby # in lib/my_gem.rb module MyGem dep = import "my_gem_dependency" # my_gem_dependency is "anchored" under the MyGem namespace, so not exposed to users # of the gem unless they also require it. MyGemDependency = dep #... end ``` There are a couple important implementation decisions in the gem: 1. _Only load code once._ When the same file is imported again (either directly or transitively), "copy" constants from previously imported namespace to the new namespace using a registry which maps which namespace (import) was used to load which file (as shown above with activerecord/activemodel). This is necessary to ensure that different imports can "see" shared files. A similar registry is used to track autoloads so that they work correctly when used from imported code. 2. Toplevel core types (`NilClass`, `TrueClass`, `FalseClass`, `String`, etc) are "aliased" to constants under each import module to make them available. Thus there can be side-effects of importing code, but this allows a gem like Rails to monkeypatch core classes which it needs to do for it to work. 3. `Object.const_missing` is patched to check the caller location and resolve to the constant defined under an import, if there is an import defined for that file. To be clear: **I think 1) should be implemented in Ruby, but not 2) and 3).** The last one (`Object.const_missing`) is a hack to support the case where a toplevel constant is referenced from a method called in imported code (at which point the `top_wrapper` is not active.) I know this is a big proposal, and there are strong opinions held. I would really appreciate constructive feedback on this general idea. Notes from September's Developers Meeting: https://github.com/ruby/dev-meeting-log/blob/master/DevMeeting-2022-09-22.md#feature-10320-require-into-module-shioyama See also similar discussion in: https://bugs.ruby-lang.org/issues/10320 -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/postorius/lists/ruby-core.ml.ruby-lang.org/