ruby-core@ruby-lang.org archive (unofficial mirror)
 help / color / mirror / Atom feed
* [ruby-core:107577] [Ruby master Feature#12962] Feature Proposal: Extend 'protected' to support module friendship
       [not found] <redmine.issue-12962.20161119131539.7674@ruby-lang.org>
@ 2022-02-14 14:23 ` Eregon (Benoit Daloze)
  2022-02-14 15:02 ` [ruby-core:107579] " matthewd (Matthew Draper)
                   ` (6 subsequent siblings)
  7 siblings, 0 replies; 8+ messages in thread
From: Eregon (Benoit Daloze) @ 2022-02-14 14:23 UTC (permalink / raw
  To: ruby-core

Issue #12962 has been updated by Eregon (Benoit Daloze).


Do you have an example in Rails?

This sounds like it can be solved by moving such private-but-shared method to `Internals` or some other module.

Calling "private/protected" methods on another object feels rather wrong to me.

Maybe `private_constant` can also be leveraged to e.g. have a private class/module but all methods on it public.

----------------------------------------
Feature #12962: Feature Proposal: Extend 'protected' to support module friendship
https://bugs.ruby-lang.org/issues/12962#change-96488

* Author: matthewd (Matthew Draper)
* Status: Open
* Priority: Normal
----------------------------------------
When working on a larger library, with many classes that have both official API methods and internal supporting methods, it can be hard to distinguish between them.

In Rails, for example, we currently do this using `:nodoc:` -- if a method is hidden from the documentation, it is not part of the officially supported API, even if it has `public` visibility.

This approach can be confusing for users, however, because they can find methods that seem to do what they want, and start calling them, without ever looking at the documentation: either by just guessing a likely method name, or even being guided to it by `did_you_mean`.

Method visibility controls seem like the right solution to this problem: if we make the methods `private` or `protected`, users can still choose to call them, but only by first acknowledging that they're using internal API. However, as we have object oriented internals, a lot of our internal API calls are between instances of unrelated classes... and using `send` on all those calls would make our own code very untidy.

I propose that the solution to this problem is to make `protected` more widely useful, by allowing a module to nominate other modules that are allowed to call its protected methods.

```ruby
class A
  protected def foo
    "secrets"
  end
end

class D
  def call_foo
    A.new.foo
  end
end
A.friend D

D.new.call_foo # => "secrets"
```

This change is backwards compatible for existing uses of `protected`: a module is always considered its own friend (so calls that previously worked will continue to do so), and classes have no other friends by default (so calls that were previously disallowed will also continue to do so).

Using a module, a library can easily establish a 'friendship group' of related classes without needing to link them individually, as well as providing a single opt-in for user code that consciously chooses to use unsupported APIs.

```ruby
module MyLib
  module Internals
  end

  class A
    include Internals
    friend Internals

    protected def foo
      "implementation"
    end
  end

  class B
    include Internals
    friend Internals

    protected def bar
      A.new.foo
    end
  end
end

class UserCode
  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

class FriendlyUserCode
  include MyLib::Internals

  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

UserCode.new.call_things # !> NoMethodError: protected method `foo'..
FriendlyUserCode.new.call_things # => ["implementation", "implementation"]
```

This change seems in keeping with the ruby philosophy that a method's visibility is more of a guideline than a strictly enforced rule -- here, we allow the callee to blur the line, instead of leaving it up to the caller to use `send`.


The implementation is surprisingly simple, and only adds time (searching an array of friends, instead of only looking for the current class) after a method call has already resolved to a protected method.


While I'm personally most interested in how this could be applied in a Rails-sized project (such as.. Rails), I believe it would provide a helpful clarifying tool to any library that has multiple collaborating classes, whose instances are also exposed to user code.




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

^ permalink raw reply	[flat|nested] 8+ messages in thread

* [ruby-core:107579] [Ruby master Feature#12962] Feature Proposal: Extend 'protected' to support module friendship
       [not found] <redmine.issue-12962.20161119131539.7674@ruby-lang.org>
  2022-02-14 14:23 ` [ruby-core:107577] [Ruby master Feature#12962] Feature Proposal: Extend 'protected' to support module friendship Eregon (Benoit Daloze)
@ 2022-02-14 15:02 ` matthewd (Matthew Draper)
  2022-02-14 15:30 ` [ruby-core:107581] " Eregon (Benoit Daloze)
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 8+ messages in thread
From: matthewd (Matthew Draper) @ 2022-02-14 15:02 UTC (permalink / raw
  To: ruby-core

Issue #12962 has been updated by matthewd (Matthew Draper).


> Calling "private/protected" methods on another object feels rather wrong to me.

The whole point of `protected` is that it allows you to call methods on another object. But I assume you meant another object that is not an instance of the caller's class.

A quick grep for `:nodoc:` turned up this example: https://github.com/ruby/ruby/blob/26187a8520b8c6645206a2064c11a7ab86a89845/lib/net/http/response.rb#L163

In my experience it's just not unusual for two collaborating objects to need to talk to each other in more detail than their "user"-facing public API intends to expose. In a typed OO language, this is where the objects communicate using a private concrete class, while only "publishing" a more focused Interface.

As in that Net::HTTP example, a separate module to stash the methods is not really viable, because we're talking about instance methods that need access to ivars -- that's why it's a method on the current target object, and not a private method on the caller.

----------------------------------------
Feature #12962: Feature Proposal: Extend 'protected' to support module friendship
https://bugs.ruby-lang.org/issues/12962#change-96490

* Author: matthewd (Matthew Draper)
* Status: Open
* Priority: Normal
----------------------------------------
When working on a larger library, with many classes that have both official API methods and internal supporting methods, it can be hard to distinguish between them.

In Rails, for example, we currently do this using `:nodoc:` -- if a method is hidden from the documentation, it is not part of the officially supported API, even if it has `public` visibility.

This approach can be confusing for users, however, because they can find methods that seem to do what they want, and start calling them, without ever looking at the documentation: either by just guessing a likely method name, or even being guided to it by `did_you_mean`.

Method visibility controls seem like the right solution to this problem: if we make the methods `private` or `protected`, users can still choose to call them, but only by first acknowledging that they're using internal API. However, as we have object oriented internals, a lot of our internal API calls are between instances of unrelated classes... and using `send` on all those calls would make our own code very untidy.

I propose that the solution to this problem is to make `protected` more widely useful, by allowing a module to nominate other modules that are allowed to call its protected methods.

```ruby
class A
  protected def foo
    "secrets"
  end
end

class D
  def call_foo
    A.new.foo
  end
end
A.friend D

D.new.call_foo # => "secrets"
```

This change is backwards compatible for existing uses of `protected`: a module is always considered its own friend (so calls that previously worked will continue to do so), and classes have no other friends by default (so calls that were previously disallowed will also continue to do so).

Using a module, a library can easily establish a 'friendship group' of related classes without needing to link them individually, as well as providing a single opt-in for user code that consciously chooses to use unsupported APIs.

```ruby
module MyLib
  module Internals
  end

  class A
    include Internals
    friend Internals

    protected def foo
      "implementation"
    end
  end

  class B
    include Internals
    friend Internals

    protected def bar
      A.new.foo
    end
  end
end

class UserCode
  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

class FriendlyUserCode
  include MyLib::Internals

  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

UserCode.new.call_things # !> NoMethodError: protected method `foo'..
FriendlyUserCode.new.call_things # => ["implementation", "implementation"]
```

This change seems in keeping with the ruby philosophy that a method's visibility is more of a guideline than a strictly enforced rule -- here, we allow the callee to blur the line, instead of leaving it up to the caller to use `send`.


The implementation is surprisingly simple, and only adds time (searching an array of friends, instead of only looking for the current class) after a method call has already resolved to a protected method.


While I'm personally most interested in how this could be applied in a Rails-sized project (such as.. Rails), I believe it would provide a helpful clarifying tool to any library that has multiple collaborating classes, whose instances are also exposed to user code.




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

^ permalink raw reply	[flat|nested] 8+ messages in thread

* [ruby-core:107581] [Ruby master Feature#12962] Feature Proposal: Extend 'protected' to support module friendship
       [not found] <redmine.issue-12962.20161119131539.7674@ruby-lang.org>
  2022-02-14 14:23 ` [ruby-core:107577] [Ruby master Feature#12962] Feature Proposal: Extend 'protected' to support module friendship Eregon (Benoit Daloze)
  2022-02-14 15:02 ` [ruby-core:107579] " matthewd (Matthew Draper)
@ 2022-02-14 15:30 ` Eregon (Benoit Daloze)
  2022-02-15 13:25 ` [ruby-core:107590] " matthewd (Matthew Draper)
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 8+ messages in thread
From: Eregon (Benoit Daloze) @ 2022-02-14 15:30 UTC (permalink / raw
  To: ruby-core

Issue #12962 has been updated by Eregon (Benoit Daloze).


For this Net::HTTP example IMHO it'd be better to use `send`, that would make it a private method and be explicit that code is calling into partly-internals.

IMHO friend-like visibility is too magic and the linear search could be a significant overhead on such calls.
`send` can be easily optimized OTOH (especially with a literal Symbol as name).

----------------------------------------
Feature #12962: Feature Proposal: Extend 'protected' to support module friendship
https://bugs.ruby-lang.org/issues/12962#change-96492

* Author: matthewd (Matthew Draper)
* Status: Open
* Priority: Normal
----------------------------------------
When working on a larger library, with many classes that have both official API methods and internal supporting methods, it can be hard to distinguish between them.

In Rails, for example, we currently do this using `:nodoc:` -- if a method is hidden from the documentation, it is not part of the officially supported API, even if it has `public` visibility.

This approach can be confusing for users, however, because they can find methods that seem to do what they want, and start calling them, without ever looking at the documentation: either by just guessing a likely method name, or even being guided to it by `did_you_mean`.

Method visibility controls seem like the right solution to this problem: if we make the methods `private` or `protected`, users can still choose to call them, but only by first acknowledging that they're using internal API. However, as we have object oriented internals, a lot of our internal API calls are between instances of unrelated classes... and using `send` on all those calls would make our own code very untidy.

I propose that the solution to this problem is to make `protected` more widely useful, by allowing a module to nominate other modules that are allowed to call its protected methods.

```ruby
class A
  protected def foo
    "secrets"
  end
end

class D
  def call_foo
    A.new.foo
  end
end
A.friend D

D.new.call_foo # => "secrets"
```

This change is backwards compatible for existing uses of `protected`: a module is always considered its own friend (so calls that previously worked will continue to do so), and classes have no other friends by default (so calls that were previously disallowed will also continue to do so).

Using a module, a library can easily establish a 'friendship group' of related classes without needing to link them individually, as well as providing a single opt-in for user code that consciously chooses to use unsupported APIs.

```ruby
module MyLib
  module Internals
  end

  class A
    include Internals
    friend Internals

    protected def foo
      "implementation"
    end
  end

  class B
    include Internals
    friend Internals

    protected def bar
      A.new.foo
    end
  end
end

class UserCode
  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

class FriendlyUserCode
  include MyLib::Internals

  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

UserCode.new.call_things # !> NoMethodError: protected method `foo'..
FriendlyUserCode.new.call_things # => ["implementation", "implementation"]
```

This change seems in keeping with the ruby philosophy that a method's visibility is more of a guideline than a strictly enforced rule -- here, we allow the callee to blur the line, instead of leaving it up to the caller to use `send`.


The implementation is surprisingly simple, and only adds time (searching an array of friends, instead of only looking for the current class) after a method call has already resolved to a protected method.


While I'm personally most interested in how this could be applied in a Rails-sized project (such as.. Rails), I believe it would provide a helpful clarifying tool to any library that has multiple collaborating classes, whose instances are also exposed to user code.




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

^ permalink raw reply	[flat|nested] 8+ messages in thread

* [ruby-core:107590] [Ruby master Feature#12962] Feature Proposal: Extend 'protected' to support module friendship
       [not found] <redmine.issue-12962.20161119131539.7674@ruby-lang.org>
                   ` (2 preceding siblings ...)
  2022-02-14 15:30 ` [ruby-core:107581] " Eregon (Benoit Daloze)
@ 2022-02-15 13:25 ` matthewd (Matthew Draper)
  2022-02-15 14:53 ` [ruby-core:107594] " p8 (Petrik de Heus)
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 8+ messages in thread
From: matthewd (Matthew Draper) @ 2022-02-15 13:25 UTC (permalink / raw
  To: ruby-core

Issue #12962 has been updated by matthewd (Matthew Draper).


The trouble with using `send` in this [general] situation is that it makes it too equally-easy to reach into _all_ internals: you want access to methods that are suitable for a collaborator but not a downstream user, but you can also immediately access truly-private methods that cause the object to violate its intended invariants. In a large project, you still want _some_ guard-rails for your collaborators, even as you give them more abilities than usual.

Widespread use of `send` is also just unpleasant to read and maintain. In practice, people don't like writing code like that: I can see additional uses of `:nodoc:` for this purpose in stdlib (the chosen example was just particularly illustrative because of its comment), yet `send(:` turns up remarkably little. Given the currently-available options, it seems that more than just Rails chooses "secretly public" methods over send+private.

On performance, `protected` already performs a linear `rb_obj_is_kind_of` search. As you note, the total search becomes O(n*m) here... but the `m` is the number of friends (so only becomes > 0 when friendship is in use), and is only even considered when the target method is protected (or, in an alternate variant of this proposal, some specific fourth method visibility level).

I would expect this check to be easier to optimize than `send`, for monomorphic call-sites, by leaning on existing call-site caching. (If a friendship change bumps the class serial, there's no need to recheck the cached call.)
Even setting aside the syntactic noise of repeated `send(:sym)`, I'm not aware of any current optimization effort that could make it competitive.

----------------------------------------
Feature #12962: Feature Proposal: Extend 'protected' to support module friendship
https://bugs.ruby-lang.org/issues/12962#change-96502

* Author: matthewd (Matthew Draper)
* Status: Open
* Priority: Normal
----------------------------------------
When working on a larger library, with many classes that have both official API methods and internal supporting methods, it can be hard to distinguish between them.

In Rails, for example, we currently do this using `:nodoc:` -- if a method is hidden from the documentation, it is not part of the officially supported API, even if it has `public` visibility.

This approach can be confusing for users, however, because they can find methods that seem to do what they want, and start calling them, without ever looking at the documentation: either by just guessing a likely method name, or even being guided to it by `did_you_mean`.

Method visibility controls seem like the right solution to this problem: if we make the methods `private` or `protected`, users can still choose to call them, but only by first acknowledging that they're using internal API. However, as we have object oriented internals, a lot of our internal API calls are between instances of unrelated classes... and using `send` on all those calls would make our own code very untidy.

I propose that the solution to this problem is to make `protected` more widely useful, by allowing a module to nominate other modules that are allowed to call its protected methods.

```ruby
class A
  protected def foo
    "secrets"
  end
end

class D
  def call_foo
    A.new.foo
  end
end
A.friend D

D.new.call_foo # => "secrets"
```

This change is backwards compatible for existing uses of `protected`: a module is always considered its own friend (so calls that previously worked will continue to do so), and classes have no other friends by default (so calls that were previously disallowed will also continue to do so).

Using a module, a library can easily establish a 'friendship group' of related classes without needing to link them individually, as well as providing a single opt-in for user code that consciously chooses to use unsupported APIs.

```ruby
module MyLib
  module Internals
  end

  class A
    include Internals
    friend Internals

    protected def foo
      "implementation"
    end
  end

  class B
    include Internals
    friend Internals

    protected def bar
      A.new.foo
    end
  end
end

class UserCode
  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

class FriendlyUserCode
  include MyLib::Internals

  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

UserCode.new.call_things # !> NoMethodError: protected method `foo'..
FriendlyUserCode.new.call_things # => ["implementation", "implementation"]
```

This change seems in keeping with the ruby philosophy that a method's visibility is more of a guideline than a strictly enforced rule -- here, we allow the callee to blur the line, instead of leaving it up to the caller to use `send`.


The implementation is surprisingly simple, and only adds time (searching an array of friends, instead of only looking for the current class) after a method call has already resolved to a protected method.


While I'm personally most interested in how this could be applied in a Rails-sized project (such as.. Rails), I believe it would provide a helpful clarifying tool to any library that has multiple collaborating classes, whose instances are also exposed to user code.




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

^ permalink raw reply	[flat|nested] 8+ messages in thread

* [ruby-core:107594] [Ruby master Feature#12962] Feature Proposal: Extend 'protected' to support module friendship
       [not found] <redmine.issue-12962.20161119131539.7674@ruby-lang.org>
                   ` (3 preceding siblings ...)
  2022-02-15 13:25 ` [ruby-core:107590] " matthewd (Matthew Draper)
@ 2022-02-15 14:53 ` p8 (Petrik de Heus)
  2022-02-15 15:14 ` [ruby-core:107595] " Eregon (Benoit Daloze)
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 8+ messages in thread
From: p8 (Petrik de Heus) @ 2022-02-15 14:53 UTC (permalink / raw
  To: ruby-core

Issue #12962 has been updated by p8 (Petrik de Heus).


IF this gets accepted I could imagine having a `protected_send` just like we have a `public_send` and `send` (which is basically `private_send`).

----------------------------------------
Feature #12962: Feature Proposal: Extend 'protected' to support module friendship
https://bugs.ruby-lang.org/issues/12962#change-96506

* Author: matthewd (Matthew Draper)
* Status: Open
* Priority: Normal
----------------------------------------
When working on a larger library, with many classes that have both official API methods and internal supporting methods, it can be hard to distinguish between them.

In Rails, for example, we currently do this using `:nodoc:` -- if a method is hidden from the documentation, it is not part of the officially supported API, even if it has `public` visibility.

This approach can be confusing for users, however, because they can find methods that seem to do what they want, and start calling them, without ever looking at the documentation: either by just guessing a likely method name, or even being guided to it by `did_you_mean`.

Method visibility controls seem like the right solution to this problem: if we make the methods `private` or `protected`, users can still choose to call them, but only by first acknowledging that they're using internal API. However, as we have object oriented internals, a lot of our internal API calls are between instances of unrelated classes... and using `send` on all those calls would make our own code very untidy.

I propose that the solution to this problem is to make `protected` more widely useful, by allowing a module to nominate other modules that are allowed to call its protected methods.

```ruby
class A
  protected def foo
    "secrets"
  end
end

class D
  def call_foo
    A.new.foo
  end
end
A.friend D

D.new.call_foo # => "secrets"
```

This change is backwards compatible for existing uses of `protected`: a module is always considered its own friend (so calls that previously worked will continue to do so), and classes have no other friends by default (so calls that were previously disallowed will also continue to do so).

Using a module, a library can easily establish a 'friendship group' of related classes without needing to link them individually, as well as providing a single opt-in for user code that consciously chooses to use unsupported APIs.

```ruby
module MyLib
  module Internals
  end

  class A
    include Internals
    friend Internals

    protected def foo
      "implementation"
    end
  end

  class B
    include Internals
    friend Internals

    protected def bar
      A.new.foo
    end
  end
end

class UserCode
  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

class FriendlyUserCode
  include MyLib::Internals

  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

UserCode.new.call_things # !> NoMethodError: protected method `foo'..
FriendlyUserCode.new.call_things # => ["implementation", "implementation"]
```

This change seems in keeping with the ruby philosophy that a method's visibility is more of a guideline than a strictly enforced rule -- here, we allow the callee to blur the line, instead of leaving it up to the caller to use `send`.


The implementation is surprisingly simple, and only adds time (searching an array of friends, instead of only looking for the current class) after a method call has already resolved to a protected method.


While I'm personally most interested in how this could be applied in a Rails-sized project (such as.. Rails), I believe it would provide a helpful clarifying tool to any library that has multiple collaborating classes, whose instances are also exposed to user code.




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

^ permalink raw reply	[flat|nested] 8+ messages in thread

* [ruby-core:107595] [Ruby master Feature#12962] Feature Proposal: Extend 'protected' to support module friendship
       [not found] <redmine.issue-12962.20161119131539.7674@ruby-lang.org>
                   ` (4 preceding siblings ...)
  2022-02-15 14:53 ` [ruby-core:107594] " p8 (Petrik de Heus)
@ 2022-02-15 15:14 ` Eregon (Benoit Daloze)
  2022-02-15 15:19 ` [ruby-core:107596] " Eregon (Benoit Daloze)
  2022-02-17 12:11 ` [ruby-core:107627] " matz (Yukihiro Matsumoto)
  7 siblings, 0 replies; 8+ messages in thread
From: Eregon (Benoit Daloze) @ 2022-02-15 15:14 UTC (permalink / raw
  To: ruby-core

Issue #12962 has been updated by Eregon (Benoit Daloze).


matthewd (Matthew Draper) wrote in #note-10:
> The trouble with using `send` in this [general] situation is that it makes it too equally-easy to reach into _all_ internals: you want access to methods that are suitable for a collaborator but not a downstream user, but you can also immediately access truly-private methods that cause the object to violate its intended invariants. In a large project, you still want _some_ guard-rails for your collaborators, even as you give them more abilities than usual.

I see. Such semi-private methods could have a comment explaining they are also used by other parts of the gem.
Or even an alias of `private` to indicate that but be clearer on the intention.

> Widespread use of `send` is also just unpleasant to read and maintain. In practice, people don't like writing code like that: I can see additional uses of `:nodoc:` for this purpose in stdlib (the chosen example was just particularly illustrative because of its comment), yet `send(:` turns up remarkably little. Given the currently-available options, it seems that more than just Rails chooses "secretly public" methods over send+private.

It might also be written as `__send__(:`.
There are not many occurrences in stdlib, but stdlibs are also fairly small compared to larger gems, and probably don't need this much.

> I would expect this check to be easier to optimize than `send`, for monomorphic call-sites, by leaning on existing call-site caching. (If a friendship change bumps the class serial, there's no need to recheck the cached call.)

Right, at least this can be cached for a call site since it's add-only.
It would likely be very difficult to cache when trying to persist JITed code though, as it depends on lots of live values.

> I'm not aware of any current optimization effort that could make it competitive.

There is #17291, but it would need a redefinition check to be semantically correct.
TruffleRuby already optimizes send and __send__, in JITed code there is no difference and only a small overhead in interpreter (when using a constant method name).

----------------------------------------
Feature #12962: Feature Proposal: Extend 'protected' to support module friendship
https://bugs.ruby-lang.org/issues/12962#change-96507

* Author: matthewd (Matthew Draper)
* Status: Open
* Priority: Normal
----------------------------------------
When working on a larger library, with many classes that have both official API methods and internal supporting methods, it can be hard to distinguish between them.

In Rails, for example, we currently do this using `:nodoc:` -- if a method is hidden from the documentation, it is not part of the officially supported API, even if it has `public` visibility.

This approach can be confusing for users, however, because they can find methods that seem to do what they want, and start calling them, without ever looking at the documentation: either by just guessing a likely method name, or even being guided to it by `did_you_mean`.

Method visibility controls seem like the right solution to this problem: if we make the methods `private` or `protected`, users can still choose to call them, but only by first acknowledging that they're using internal API. However, as we have object oriented internals, a lot of our internal API calls are between instances of unrelated classes... and using `send` on all those calls would make our own code very untidy.

I propose that the solution to this problem is to make `protected` more widely useful, by allowing a module to nominate other modules that are allowed to call its protected methods.

```ruby
class A
  protected def foo
    "secrets"
  end
end

class D
  def call_foo
    A.new.foo
  end
end
A.friend D

D.new.call_foo # => "secrets"
```

This change is backwards compatible for existing uses of `protected`: a module is always considered its own friend (so calls that previously worked will continue to do so), and classes have no other friends by default (so calls that were previously disallowed will also continue to do so).

Using a module, a library can easily establish a 'friendship group' of related classes without needing to link them individually, as well as providing a single opt-in for user code that consciously chooses to use unsupported APIs.

```ruby
module MyLib
  module Internals
  end

  class A
    include Internals
    friend Internals

    protected def foo
      "implementation"
    end
  end

  class B
    include Internals
    friend Internals

    protected def bar
      A.new.foo
    end
  end
end

class UserCode
  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

class FriendlyUserCode
  include MyLib::Internals

  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

UserCode.new.call_things # !> NoMethodError: protected method `foo'..
FriendlyUserCode.new.call_things # => ["implementation", "implementation"]
```

This change seems in keeping with the ruby philosophy that a method's visibility is more of a guideline than a strictly enforced rule -- here, we allow the callee to blur the line, instead of leaving it up to the caller to use `send`.


The implementation is surprisingly simple, and only adds time (searching an array of friends, instead of only looking for the current class) after a method call has already resolved to a protected method.


While I'm personally most interested in how this could be applied in a Rails-sized project (such as.. Rails), I believe it would provide a helpful clarifying tool to any library that has multiple collaborating classes, whose instances are also exposed to user code.




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

^ permalink raw reply	[flat|nested] 8+ messages in thread

* [ruby-core:107596] [Ruby master Feature#12962] Feature Proposal: Extend 'protected' to support module friendship
       [not found] <redmine.issue-12962.20161119131539.7674@ruby-lang.org>
                   ` (5 preceding siblings ...)
  2022-02-15 15:14 ` [ruby-core:107595] " Eregon (Benoit Daloze)
@ 2022-02-15 15:19 ` Eregon (Benoit Daloze)
  2022-02-17 12:11 ` [ruby-core:107627] " matz (Yukihiro Matsumoto)
  7 siblings, 0 replies; 8+ messages in thread
From: Eregon (Benoit Daloze) @ 2022-02-15 15:19 UTC (permalink / raw
  To: ruby-core

Issue #12962 has been updated by Eregon (Benoit Daloze).


p8 (Petrik de Heus) wrote in #note-11:
> I can imagine having a `protected_send` just like we have a `public_send` and `send` (which is basically `private_send`).

Yeah, I was also thinking to something like:
```ruby
friend/internal/protected def my_method
  ...
end

def foo
  obj.send_friend/send_internal/send_protected(:my_method)
end
```
and such method could only be called by `send_friend/send_internal/send_protected`, and that would be very efficient and simple to check.

I don't think it's hard to optimize `send` and similar, e.g. there could be an extra call cache when parsing send/`__send__`/etc, and that could be used to cache the second lookup too.

----------------------------------------
Feature #12962: Feature Proposal: Extend 'protected' to support module friendship
https://bugs.ruby-lang.org/issues/12962#change-96508

* Author: matthewd (Matthew Draper)
* Status: Open
* Priority: Normal
----------------------------------------
When working on a larger library, with many classes that have both official API methods and internal supporting methods, it can be hard to distinguish between them.

In Rails, for example, we currently do this using `:nodoc:` -- if a method is hidden from the documentation, it is not part of the officially supported API, even if it has `public` visibility.

This approach can be confusing for users, however, because they can find methods that seem to do what they want, and start calling them, without ever looking at the documentation: either by just guessing a likely method name, or even being guided to it by `did_you_mean`.

Method visibility controls seem like the right solution to this problem: if we make the methods `private` or `protected`, users can still choose to call them, but only by first acknowledging that they're using internal API. However, as we have object oriented internals, a lot of our internal API calls are between instances of unrelated classes... and using `send` on all those calls would make our own code very untidy.

I propose that the solution to this problem is to make `protected` more widely useful, by allowing a module to nominate other modules that are allowed to call its protected methods.

```ruby
class A
  protected def foo
    "secrets"
  end
end

class D
  def call_foo
    A.new.foo
  end
end
A.friend D

D.new.call_foo # => "secrets"
```

This change is backwards compatible for existing uses of `protected`: a module is always considered its own friend (so calls that previously worked will continue to do so), and classes have no other friends by default (so calls that were previously disallowed will also continue to do so).

Using a module, a library can easily establish a 'friendship group' of related classes without needing to link them individually, as well as providing a single opt-in for user code that consciously chooses to use unsupported APIs.

```ruby
module MyLib
  module Internals
  end

  class A
    include Internals
    friend Internals

    protected def foo
      "implementation"
    end
  end

  class B
    include Internals
    friend Internals

    protected def bar
      A.new.foo
    end
  end
end

class UserCode
  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

class FriendlyUserCode
  include MyLib::Internals

  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

UserCode.new.call_things # !> NoMethodError: protected method `foo'..
FriendlyUserCode.new.call_things # => ["implementation", "implementation"]
```

This change seems in keeping with the ruby philosophy that a method's visibility is more of a guideline than a strictly enforced rule -- here, we allow the callee to blur the line, instead of leaving it up to the caller to use `send`.


The implementation is surprisingly simple, and only adds time (searching an array of friends, instead of only looking for the current class) after a method call has already resolved to a protected method.


While I'm personally most interested in how this could be applied in a Rails-sized project (such as.. Rails), I believe it would provide a helpful clarifying tool to any library that has multiple collaborating classes, whose instances are also exposed to user code.




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

^ permalink raw reply	[flat|nested] 8+ messages in thread

* [ruby-core:107627] [Ruby master Feature#12962] Feature Proposal: Extend 'protected' to support module friendship
       [not found] <redmine.issue-12962.20161119131539.7674@ruby-lang.org>
                   ` (6 preceding siblings ...)
  2022-02-15 15:19 ` [ruby-core:107596] " Eregon (Benoit Daloze)
@ 2022-02-17 12:11 ` matz (Yukihiro Matsumoto)
  7 siblings, 0 replies; 8+ messages in thread
From: matz (Yukihiro Matsumoto) @ 2022-02-17 12:11 UTC (permalink / raw
  To: ruby-core

Issue #12962 has been updated by matz (Yukihiro Matsumoto).

Status changed from Open to Rejected

I revisited this issue at the developer meeting and concluded to reject this proposal.
I understand OP's need for the `friend` visibility but considering the dynamic nature of the language (plus current usage of `protected` visibility), this visibility provides the value far less than the pain of complexity. I'd rather like to remove `protected` visibility, if possible (I know it's too late).

Thus this is rejected. Sorry.

Matz.


----------------------------------------
Feature #12962: Feature Proposal: Extend 'protected' to support module friendship
https://bugs.ruby-lang.org/issues/12962#change-96538

* Author: matthewd (Matthew Draper)
* Status: Rejected
* Priority: Normal
----------------------------------------
When working on a larger library, with many classes that have both official API methods and internal supporting methods, it can be hard to distinguish between them.

In Rails, for example, we currently do this using `:nodoc:` -- if a method is hidden from the documentation, it is not part of the officially supported API, even if it has `public` visibility.

This approach can be confusing for users, however, because they can find methods that seem to do what they want, and start calling them, without ever looking at the documentation: either by just guessing a likely method name, or even being guided to it by `did_you_mean`.

Method visibility controls seem like the right solution to this problem: if we make the methods `private` or `protected`, users can still choose to call them, but only by first acknowledging that they're using internal API. However, as we have object oriented internals, a lot of our internal API calls are between instances of unrelated classes... and using `send` on all those calls would make our own code very untidy.

I propose that the solution to this problem is to make `protected` more widely useful, by allowing a module to nominate other modules that are allowed to call its protected methods.

```ruby
class A
  protected def foo
    "secrets"
  end
end

class D
  def call_foo
    A.new.foo
  end
end
A.friend D

D.new.call_foo # => "secrets"
```

This change is backwards compatible for existing uses of `protected`: a module is always considered its own friend (so calls that previously worked will continue to do so), and classes have no other friends by default (so calls that were previously disallowed will also continue to do so).

Using a module, a library can easily establish a 'friendship group' of related classes without needing to link them individually, as well as providing a single opt-in for user code that consciously chooses to use unsupported APIs.

```ruby
module MyLib
  module Internals
  end

  class A
    include Internals
    friend Internals

    protected def foo
      "implementation"
    end
  end

  class B
    include Internals
    friend Internals

    protected def bar
      A.new.foo
    end
  end
end

class UserCode
  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

class FriendlyUserCode
  include MyLib::Internals

  def call_things
    [MyLib::A.new.foo, MyLib::B.new.bar]
  end
end

UserCode.new.call_things # !> NoMethodError: protected method `foo'..
FriendlyUserCode.new.call_things # => ["implementation", "implementation"]
```

This change seems in keeping with the ruby philosophy that a method's visibility is more of a guideline than a strictly enforced rule -- here, we allow the callee to blur the line, instead of leaving it up to the caller to use `send`.


The implementation is surprisingly simple, and only adds time (searching an array of friends, instead of only looking for the current class) after a method call has already resolved to a protected method.


While I'm personally most interested in how this could be applied in a Rails-sized project (such as.. Rails), I believe it would provide a helpful clarifying tool to any library that has multiple collaborating classes, whose instances are also exposed to user code.




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

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2022-02-17 12:11 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
     [not found] <redmine.issue-12962.20161119131539.7674@ruby-lang.org>
2022-02-14 14:23 ` [ruby-core:107577] [Ruby master Feature#12962] Feature Proposal: Extend 'protected' to support module friendship Eregon (Benoit Daloze)
2022-02-14 15:02 ` [ruby-core:107579] " matthewd (Matthew Draper)
2022-02-14 15:30 ` [ruby-core:107581] " Eregon (Benoit Daloze)
2022-02-15 13:25 ` [ruby-core:107590] " matthewd (Matthew Draper)
2022-02-15 14:53 ` [ruby-core:107594] " p8 (Petrik de Heus)
2022-02-15 15:14 ` [ruby-core:107595] " Eregon (Benoit Daloze)
2022-02-15 15:19 ` [ruby-core:107596] " Eregon (Benoit Daloze)
2022-02-17 12:11 ` [ruby-core:107627] " matz (Yukihiro Matsumoto)

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).