ruby-core@ruby-lang.org archive (unofficial mirror)
 help / color / mirror / Atom feed
From: loic.nageleisen@gmail.com
To: ruby-core@ruby-lang.org
Subject: [ruby-core:88957] [Ruby trunk Feature#14982] Improve namespace system in ruby to avoiding top-level names chaos
Date: Wed, 12 Sep 2018 07:48:15 +0000 (UTC)	[thread overview]
Message-ID: <redmine.journal-73990.20180912074814.2693d24c8c9598a5@ruby-lang.org> (raw)
In-Reply-To: redmine.issue-14982.20180811042606@ruby-lang.org

Issue #14982 has been updated by lloeki (Loic Nageleisen).


Please allow me to humbly present [this design and implementation of import semantics](https://gitlab.com/lloeki/pak) aiming to solve precisely the current issue, taking inspiration from Go, Python, and Node.

Basically it implements a `Package` class inheriting `Module` and leveraging the `Kernel#load` ability to `wrap` with its second argument, together with `module_eval`.
With it you can currently do:

~~~ruby
import('foo')                    # import package file as `foo`
import('foo/baz')                # import nested package file as `baz`
import('foo', to: :method)       # make available as method `foo` using a side effect (default)
import('foo', to: :const)        # make available as constant `Foo` using a side effect
import('foo', to: :local)        # (attempt to, see below) make available as local variable `foo` using a side effect
import('foo', as: :bar)          # rename the target set as a side effect to `bar`, can be combined with `to:`
f= import('foo', to: :value)     # no side effect, explicit assignment to a local var
Foo = import('foo', to: :value)  # no side effect, explicit assignment to a local const
def foo; import('foo', to: :value); end  # you get the idea
~~~

More examples are available in the README and in `test.rb` along with the `test` fixture tree (those are not unit tests though, apologies for the bad naming on my part).

The various possibilities of `to:` (`nil`, `:method`, `:const`, `:local`) are to explore the possible ways to make the module available to the caller.

The implementation works on current Ruby, and would work even better [save for a limitation](https://gitlab.com/lloeki/pak#wishlist-setting-locals-directly) of `bind_local_variable_set`. It requires the `binding_of_caller` gem in some situations but it is not a strict requirement (not needed with `to: :value`).

The package file does not contains a declaration of the Package instance (as `class` and `module` do), as it will be deduced from the file name, the goal being to box and isolate automatically the file contents, and not requiring new (IMHO awkward) keywords like `isolate`. Maybe using a file extension such as `.rbp` to distinguish `.rb` files written to be `require`d from those to be `import`ed could be useful, but it is definitely not mandatory. Maybe a `package` keyword (like in Go) at the start could be useful to guard against the file being `require`d. but again, this is not mandatory. Code leveraging this design also has the advantage to eliminate indentation, repetition, and boilerplate that is typically present in deeply nested ruby files when conventionally matching the file and directory names with the module and class names.

What do you think?

----------------------------------------
Feature #14982: Improve namespace system in ruby to avoiding top-level  names chaos
https://bugs.ruby-lang.org/issues/14982#change-73990

* Author: jjyr (Jinyang Jiang)
* Status: Open
* Priority: Normal
* Assignee: matz (Yukihiro Matsumoto)
* Target version: Next Major
----------------------------------------
Updated: https://bugs.ruby-lang.org/issues/14982#note-5



## Why

Ruby has evaluation all class/module names in top-level context(aka TOPLEVEL_BINDING). 
As a user we basically hard to know how many names in the current context, is causing chaos in some cases. For example:

case 1: 

Put common used errors class in a single file, like below

``` ruby
# utils/errors.rb

class FooError
end

class BarError
end
```

In other files under 'utils' we want to use those errors, so the best practice is to use `require_relative 'errors'` in each file we need.

``` ruby
# utils/binary_helper.rb

# we forget require errors

module BinaryHelper
# ...
  raise BarError
# ...
end

```

But sometime we may forget to require dependencies in a file, it's hard to notice because
 if RubyVM already execute the requires we still can access the name BarError,

but if user directly to require 'utils/binary_helper', he/she will got an NameError.


case 2: 

Two gems use same top-level module name, so we can't use them together

## The Reason of The Problem

The reason is we let module author to decision which module user can use. ('require' is basically evaluation, highly dependent on the module author's design)

But we should let users control which names to use and available in context. As many other popular languages dose(Rust, Python..)

I think the solution is basically the same philosophy compares to refinement feature.


## The Design

I propose an improved namespace to Ruby, to solve the problems and still compatible with the current Ruby module system.

``` ruby
class Foo
end

# introduce Kernel#namespace
namespace :Hello do
  # avoiding namespace chaos
  # Foo -> NameError, can't access TOPLEVEL_BINDING directly
  
  # Kernel#import method, introduce Foo name from TOPLEVEL_BINDING
  import :Foo

  # in a namespace user can only access imported name
  Foo

  # import constant to another alias name
  # can avoid writing nested module/class names
  import :"A::B::C::D", as: :E

  # require then import, for convenient 
  import :"A::B::C::D", as: :E, from: 'some_rb_file'

  # import same name from two gems
  import :"Foo", as: :Foo_A, from: 'foo_a'
  import :"Foo", as: :Foo_B, from: 'foo_b'

  # import names in batch
  import %i{"A::B::C::D", "AnotherClass"}, from: 'some_rb_file'

  # import and alias in batch
  import {:"A::B::C::D" => :E, :Foo => Foo2}, from: 'some_rb_file'

  class Bar
    def xxx
      # can access all names in namespace scope
      [Foo, Foo_A, Foo_B]
    end
  end
end

Hello.class #  -> module. namespace is just a module
Hello::Bar # so we do not broken current ruby module design

# namespace system is intent to let user to control names in context
# So user can choose use the old require way

require 'hello'

Hello::Bar


# Or user can use namespace system as we do in hello.rb

namespace :Example do
  import :"Hello::Bar", as: :Bar
  Bar # ok
  Foo # name error, cause we do not import Foo in :Example namespace
end

Foo # ok, cause Foo is loaded in TOPLEVEL_BINDING

# define nested namespace

# more clear syntax than “module Example::NestedExample”
namespace :NestedExample, under: Example do
end

namespace :Example2 do
  namespace :NestedExample do
  end
end

```

Pros:

* Completely compatible with the current module system, a gem user can completely ignore whether a gem is write in Namespace or not.
* User can completely control which names in current context/scope.
* May solve the top module name conflict issue(depends on VM implementation).
* Avoid introducing new keyword and syntax.
* Type hint or name hint can be more accuracy under namespace(not sure).

Cons:

* Need to modify Ruby VM to support the feature.




-- 
https://bugs.ruby-lang.org/

  parent reply	other threads:[~2018-09-12  7:48 UTC|newest]

Thread overview: 19+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
     [not found] <redmine.issue-14982.20180811042606@ruby-lang.org>
2018-08-11  4:26 ` [ruby-core:88446] [Ruby trunk Feature#14982] Introduce new namespace system to ruby to avoiding top-level names chaos jjyruby
2018-08-11 12:41 ` [ruby-core:88449] [Ruby trunk Feature#14982] Improve namespace system in " shevegen
2018-08-12  9:12 ` [ruby-core:88454] " jjyruby
2018-08-13  1:12 ` [ruby-core:88458] " shyouhei
2018-08-13  3:34 ` [ruby-core:88459] " merch-redmine
2018-08-13  6:40 ` [ruby-core:88461] " jjyruby
2018-08-13  7:05 ` [ruby-core:88462] " jjyruby
2018-08-16 21:02 ` [ruby-core:88506] " jjyruby
2018-08-23  7:25 ` [ruby-core:88612] " ko1
2018-09-12  7:48 ` loic.nageleisen [this message]
2018-11-22 15:04 ` [ruby-core:89974] " ciconia
2018-11-23 13:38 ` [ruby-core:90006] " v.ondruch
2018-11-23 14:26 ` [ruby-core:90007] " ciconia
2018-11-24 17:04 ` [ruby-core:90041] " v.ondruch
2018-11-24 20:35 ` [ruby-core:90048] " eregontp
2018-11-24 21:05 ` [ruby-core:90050] " v.ondruch
2018-11-24 21:09 ` [ruby-core:90051] " v.ondruch
2018-11-24 21:40 ` [ruby-core:90052] " ciconia
2019-03-23  1:10 ` [ruby-core:91953] " chocolate

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-list from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.ruby-lang.org/en/community/mailing-lists/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=redmine.journal-73990.20180912074814.2693d24c8c9598a5@ruby-lang.org \
    --to=ruby-core@ruby-lang.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).