Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encapsulated Require System #140

Closed
trans opened this issue Jun 2, 2014 · 15 comments
Closed

Encapsulated Require System #140

trans opened this issue Jun 2, 2014 · 15 comments

Comments

@trans
Copy link

trans commented Jun 2, 2014

While Ruby's require system is nice for it simplicity, it is seriously lacking for encapsulation. It would be nice to see a system that supported this.

To clarify what I am talking about, instead of loading code at the top level, one would assign the required code and then access it via a variable or constant. To differentiate this from require, lets call it "acquire". e.g.

 MyFoo = acquire('foo')
 MyFoo::Foo.new

This is like the require systems used by Lua and Node.js and such.

@vendethiel
Copy link
Contributor

I admit that it would be really sweet, but it definitely doesn't feel very ruby-esque due to monkey patching etc.

@dziulius
Copy link

dziulius commented Jun 2, 2014

I need that feature once a year at most, while it adds some ugliness to code (node's exports I'm looking at you). In ruby world such problem is close to non-existent, and various attempts (refinements) to fix non-existent problem are just waste of time.

@vendethiel
Copy link
Contributor

I definitely prefer nodejs modules to globals.

@asterite
Copy link
Member

asterite commented Jun 2, 2014

What @Nami-Doc says is the main reason Ruby doesn't implement this: monkey-patching. In any file you can reopen any other class. What would a file that reopen a class return?

Ruby is very comfortable to program. Yes, you have to be careful not to have name conflicts. But people come up with very strange (sometimes funny) names for gems (i.e.: decent_exposure, thor, etc.) so that conflict is very rare.

@trans
Copy link
Author

trans commented Jun 2, 2014

This doesn't necessarily eliminate monkey patching. It just means it would have to be done through a different means than require. e.g. maybe

MyFoo = acquire('foo')
apply_patch MyFoo

Also, there's a possibility that Ruby itself might change in the future to utilize refinements for monkey patching. See https://bugs.ruby-lang.org/issues/9704.

@sporto
Copy link

sporto commented Nov 26, 2014

I second something like this, please don't copy the Ruby way of requiring everything in the global namespace. I have had multiple conflicts because of this, like several gems trying to use the 'Search' namespace. Also, the ruby way makes it very hard to load multiple version of the same library, Node has no issue with this.

Even if I have to type some more, I far prefer the system of requiring what I need in each file (an keeping this local). The benefits overweight the drawbacks.

@jhass
Copy link
Member

jhass commented Jun 16, 2015

-1

I think it just doesn't fit the semantics of the language as #767 and similar show, disambiguating which monkey patch should affect what, how which constant should be looked up after namespacing etc while not damaging the open class system is just too hard, for both the compiler but also the user of the language.

I therefore vote to reject this, drop the existing namespacing behaviour that require has and to disallow require anywhere outside the toplevel.

@ysbaddaden
Copy link
Contributor

-1 for me too.

I stand with @jhass. I vote for require to always be global, to avoid confusion and inconsistencies. When I open a file, I want to see the actual namespaces and classes when I'll require it, either directly or throught another require. Non global requires may only make sense within ifdef or maybe inside macros.

BTW: refinements in Ruby have been there for 2 years. I feel that nobody uses them, and everybody forgot about them...

@deathbeam
Copy link

+1 I second this as I am coming here from Lua background, which had this system for globals from requires before, but now it is deprecated. What about syntax like this:

require(`foo`) as Bar

So it will require it as alias but it will still have Ruby-styled principe for globals from requires.

@jhass
Copy link
Member

jhass commented Jun 16, 2015

Side note: Refinements do change the global view but localize that change to a single file, that's vastly different from changing the namespace of things and something I wouldn't oppose.

@asterite
Copy link
Member

asterite commented Aug 5, 2015

I'm closing this, we won't change the require system. We might introduce a concept similar to refinements if we later find the need for it.

@trans
Copy link
Author

trans commented Sep 25, 2016

😞

@rdp
Copy link
Contributor

rdp commented Sep 29, 2016

(I guess the proposal here is to do what python does, which is everything is defined in a "file local" namespace, except vars declared global, +1 here):

Hmm it seems to me like monkey patching can still work

require "a/b", "B"

def B.my_new_method

end

ref: https://groups.google.com/d/msg/crystal-lang/Yid3gQnlKyI/oS4eLDh2AAAJ

@ozra
Copy link
Contributor

ozra commented Mar 12, 2017

This is a very good thing (TM) - but it should allow flexibility.

Some propositions

  • Requiring a module is "assigned" to a module/namespace reference which is by default local to the file
  • The module can be "opened" via the usual include/extend - local to file to avoid prefixing.
  • The module can also be explicitly exposed program wide, and of course "opened" program wide, if one really wants too (working the Ruby-way).
  • Any decent library should require modules into local space - thereby not leaking deps in to global space for discriminating users (with the obvious exception of libs one definitely want super accessible in more DSL'ish scenarios - specs or so)
  • Monkey patching works just as well as before and are still global (any module required is re-used over and over, if required again in the same program, so any patches done to it, even though through a local "namespace/module reference", is accessible in all units requiring it.)
    • this is how monkeypatching is done in Lua and NodeJS (even though "modules" in them are of course just common table/hash-values - in Crystal it is resolved on a compile time "level").
    • the main benefit is still knowing which modules a file rely on
    • optimally it should error if symbols/signatures are used in a file which are not defined/declared in any of the files part of its requirements (location is accessible in AST, as long as original-def-location is accessible for a node, this should be easily checked)
      • that is monkey patching is global - but - added features (new method signatures, etc.) aren't accessible unless the patching modules/files are required also
      • redefined methods/what-not of a module are used with no complaint (allowing monkey patching methods with, say, error-analysis / whatever, and all files requiring only the original module will still be using them without erroring that they're redefined outside of original requirement)
  • A using ("with") clause which I've proposed before, it also very useful: "opening" modules in an explicit lexical using scope.

Motivations

  • Making sure every file specifies its' requirements is very healthy for dependency and program integrity and quickly reveals structural problems.
  • Makes it much easier to ensure separation of concerns and refactoring code into packages
    • Continually refactoring large codebases into sub-packages is a very desirable process
    • we want to avoid large monolith sourcebases with interdependencies to-and-fro all over the place
    • improves testability, narrows range of possible erroneous combinations of invocation
    • makes it easier to share distinct functionalities increasing usage exposure and thereby also contributions - speeding up evolvment of the entire lib/program.
  • For those who don't like it, or for prototyping / whipping up a quick "just need it to do its' shit"-cli-command one can simply have a "main-include" or directly in a main-file, all requirements program-globally exposed.
  • Alternatively - though loosing a lot of promise of security would be to have it work like now by default and add a require-modifier to make the references file-local.
    • Any unwanted "leaks" would then have to be identified with a flag to the compiler (--hints, --warnings or such)

Problems

  • Since inferred type-unions still are decided to be "nearest common ancestor" rather than "precise", one will still get errors in a file requiring all its deps if additional sub-types has been defined from some type and it doesn't handle them in precisely restricted methods. But this problem is not related to this proposition - I mention it only because it might be "more surprising" when having specified all requirements.

I see only pros to extending Crystal with this stabilizing feature.

@chocolateboy
Copy link
Contributor

Almost all of the comments here (including, confusingly, later comments by the OP) are (negative) responses to a proposal to change the semantics of require, which are understandable, but not what this issue initially proposed:

To differentiate this from require, lets call it "acquire".

Since this discussion has wandered away from this topic, I've opened a new one here to discuss implementing a new method which complements require rather than replacing it.

@crystal-lang crystal-lang locked and limited conversation to collaborators May 25, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests