Project

General

Profile

Actions

Feature #17314

closed

Provide a way to declare visibility of attributes defined by attr* methods in a single expression

Added by radarek (Radosław Bułat) 6 months ago. Updated 4 months ago.

Status:
Closed
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:100763]

Description

Description

Many of us (me included) declare class attributes, even if they are private, on the top of class definition. When reading source code it's convinient to see what kind of attributes class has.

To declare private attributes we can:

  • declare them with one of attr* methods and later change visiblity calling private
  • call private without argument, then declare attributes and finally call (in most cases) public to keep defining public methods
  • declare attribute on top of the class but make them private in private section later in a file
clsss Foo
  attr_accessor :foo
  private :foo, :foo= # we have to remember about :foo= too

  private

  attr_accessor :bar

  public

  # rest of the code
end

To simplify it and create other possibilites I propose to:

  • change attr* methods so as they return array of defined methods names
  • allow public/protected/private methods to receive array of methods names (single argument)

With requested feature we could write code like this:

class Foo
  private attr_accessor :foo, :bar
end

Additionaly you could use attr* with your own methods. Something like this:

class Module
  def traceable(names)
    # ...
    names
  end
end

class Foo
  traceable attr_accessor :foo
  # it can be mixed with public/protected/private too
  protected traceable attr_accessor :bar
end

Backward compatibility

  • attr* methods currently return nil so there should be no problem with changing them
  • public/protected/private methods receive multiple positional arguments and convert all non symbol/string objects to strings. I can imagine only one case where compatibility would be broken:
class Foo
  def foo; end
  def bar; end

  arr = [:foo]
  def arr.to_str
    'bar'
  end

  private arr
end

p [Foo.public_instance_methods(false), Foo.private_instance_methods(false)]

Currently [[:foo], [:bar]] would be displayed, [[:bar], [:foo]] after requested feature is implemented.

Implementation

You can view my implementation in this (draft) PR: https://github.com/ruby/ruby/pull/3757


Related issues

Related to Ruby master - Feature #6470: Make attr_accessor return the list of generated methodClosedmatz (Yukihiro Matsumoto)Actions

Updated by marcandre (Marc-Andre Lafortune) 6 months ago

I think this is a reasonable request that could be helpful. I've been tempted a few times to write this.

Updated by sawa (Tsuyoshi Sawada) 6 months ago

Private attribute methods defeat the purpose of attribute methods. If you want to access them within the class, you can directly access the instance variables.

[I]t's convenient to see what kind of attributes [a] class has

For that purpose, you can write that as a comment in the code. Defining methods just for that purpose is waste of resource.

Updated by marcandre (Marc-Andre Lafortune) 6 months ago

Private attribute methods defeat the purpose of attribute methods

I should have mentioned it, but it is more useful for protected than private.

Updated by zverok (Victor Shepelev) 6 months ago

Private attribute methods defeat the purpose of attribute methods. If you want to access them within the class, you can directly access the instance variables.

The idea behind private attribute methods (which I've seen used in a lot of codebases, and use myself) is uniformity of the call-sequence of different values. I believe Avdi Grimm had a nice article on this point of view (will try to find it).

Basically, if in some "service" (command, operation) class we have a code like this:

class MyCommand
  def initialize(name)
    @name = name
  end

  def call
    path = File.join(BASE_PATH, @name)

    # a lot of processing done with path
    data = File.read(path)
  end

  private

  # some helper methods
end

Now, there are several ways this code may evolve:

For example (1), path calculation more complicated and extracted to its own method

def path
  File.join(BASE_PATH, @name)
end

def call
  # a lot of processing done with path
  data = File.read(path)
end

...or, if the calculation is heavy, it is also memoized (2)

def path
  @path ||= File.join(BASE_PATH, @name)
end

def call
  # a lot of processing done with path
  data = File.read(path)
end

Or -- to the point to this ticket -- suddenly we have a refactoring when the whole path is passed by class' client (3):

class MyCommand
  def initialize(path)
    @path = path
  end

  def call
    # a lot of processing done with path
    data = File.read(path)
  end

  private

  attr_reader :path

  # some helper methods
end

The important point here is: in either case, code working with just path, continues to work. There is no point in rewriting all relevant statements with @path in case (3).

More realistically, we might start with instance variable, and then calculation of it will become more complicated -- but due to the same reasons, it is reasonable to immediately start using private attr_reader, not "naked" instance var.

Updated by radarek (Radosław Bułat) 6 months ago

sawa (Tsuyoshi Sawada) wrote in #note-2:

Private attribute methods defeat the purpose of attribute methods. If you want to access them within the class, you can directly access the instance variables.

[I]t's convenient to see what kind of attributes [a] class has

For that purpose, you can write that as a comment in the code. Defining methods just for that purpose is waste of resource.

What kind of resource, cpu? But there are other resources involved which is more importand - humans. Using private attributes gives you more flexibility and it's easier to refactor.

If you use accessor methods you can later refactor them without checking where instance variables are used.

class Foo
  attr_accessor :bar
  private :bar, :bar=
end

Later you decided to change setter so it validates value

class Foo
  attr_reader :bar
  private :bar

  private def bar=(value)
    raise ArgumentError if if value < 0
    @bar = value
  end
end

It also works with subclassing. If you access or change your state through accessors methods then it's much easier to refactor you base class without worrying about other places.

zverok (Victor Shepelev) wrote in #note-4:

The idea behind private attribute methods (which I've seen used in a lot of codebases, and use myself) is uniformity of the call-sequence of different values. I believe Avdi Grimm had a nice article on this point of view (will try to find it).

I guess you are talking about this Ruby Tapas episode: https://www.rubytapas.com/2018/06/05/barewords/

Updated by zverok (Victor Shepelev) 6 months ago

The idea behind private attribute methods (which I've seen used in a lot of codebases, and use myself) is uniformity of the call-sequence of different values. I believe Avdi Grimm had a nice article on this point of view (will try to find it).

I guess you are talking about this Ruby Tapas episode: https://www.rubytapas.com/2018/06/05/barewords/

Indeed! Thanks.

Updated by Dan0042 (Daniel DeLorme) 6 months ago

+1

Ever since ruby 2.1 when def was improved to return a Symbol, I've always wondered why the same was not done for attr_* methods.

Updated by marcandre (Marc-Andre Lafortune) 6 months ago

I would also like alias_method x, y to return x so we could write protected alias_method :foo, :bar

Updated by phluid61 (Matthew Kerwin) 6 months ago

Dan0042 (Daniel DeLorme) wrote in #note-7:

+1

Ever since ruby 2.1 when def was improved to return a Symbol, I've always wondered why the same was not done for attr_* methods.

attr_accessor :x, :y  #=> ?

Updated by radarek (Radosław Bułat) 6 months ago

marcandre (Marc-Andre Lafortune) wrote in #note-8:

I would also like alias_method x, y to return x so we could write protected alias_method :foo, :bar

Additionaly both activesupport's delegate and ruby's def_delegators methods already return arrays of defined methods, which would allow you to define private/protected delegators like this:

class User < ActiveRecord::Base
  has_one :profile

  private delegate :date_of_birth, to: :profile
  private def_delegators :profile, :date_of_birth
end

Updated by radarek (Radosław Bułat) 6 months ago

phluid61 (Matthew Kerwin) wrote in #note-9:

attr_accessor :x, :y  #=> ?

This is exactly why I created this feature request. Improved attr_accessor returns array of defined methods and private/protected/public receives single array as argument.

Updated by marcandre (Marc-Andre Lafortune) 5 months ago

A Ruby summary:

class Foo
  protected [:x, :y]
  # same as:
  protected :x, :y

  attr_accessor :foo, :bar # => [:foo, :foo=, :bar, :bar=] instead of `nil`
  attr_reader :foo, :bar # => [:foo, :bar] instead of `nil`
  attr_writer :foo, :bar # => [:foo=, :bar=] instead of `nil`

  alias_method :new_alias, :existing_method # => :new_alias instead of `Foo`
end

Updated by matz (Yukihiro Matsumoto) 5 months ago

I have a small concern about compatibility. But basically agree. Go ahead.

Matz.

Actions #14

Updated by nobu (Nobuyoshi Nakada) 5 months ago

  • Status changed from Open to Closed

Applied in changeset git|34f06062174882a98ebef998c50ad8d4f7fc0f2e.


Added tests for [Feature #17314]

Updated by mame (Yusuke Endoh) 4 months ago

Hi, I've just noticed that this changeset allows not only Module#private but also Module#private_class_method to accept an array of symbols. Module#public_class_method too.

class Foo
  def self.foo; end
  def self.bar; end  
  private_class_method [:foo, :bar] # No error
end

And toplevel Kernel#private and Kernel#public do too.

def foo = nil
def bar = nil

private [:foo, :bar]
public [:foo, :bar]

I cannot think of a case where we want to pass an array of symbols to public_class_method and toplevel public. (Note that there is no toplevel attr_reader.) So I believe these changes are not intentional. But it may be more consistent for the methods to allow a symbol array. I have no opinion whether the additional methods should allow or deny an array. What do you think?

Anyway, I've added a rdoc, tests, and NEWS for the extra methods at 3a81daaf8dc037057d96b8e8cdc6ab1691e7e9d9.

Updated by marcandre (Marc-Andre Lafortune) 4 months ago

It wasn't intentional but I think it is better this way:

  • It is consistent
  • Allows users to define methods to be chained (class_attr_reader? class_cache?) that could return array of symbols.
  • no downside that I can see.

Updated by zverok (Victor Shepelev) 4 months ago

I cannot think of a case where we want to pass an array of symbols to public_class_method

Generally, there could be other DSLs that will define methods. Rails have mattr_{reader,writer,accesor} (for module attributes), which currently does not return the "proper" value to pass to public_class_method, but they can be change to do so. Then, this would make sense:

class A
  private_class_method mattr_reader :some, :internals
end
Actions #18

Updated by k0kubun (Takashi Kokubun) 27 days ago

  • Related to Feature #6470: Make attr_accessor return the list of generated method added
Actions

Also available in: Atom PDF