6

I was trying to create a class that has a private class method. I want this private class method available to be used inside an instance method.

The following was my first attempt:

class Animal
  class << self
    def public_class_greeter(name)
      private_class_greeter(name)
    end

  private
    def private_class_greeter(name)
      puts "#{name} greets private class method"
    end
  end

  def public_instance_greeter(name)
    self.class.private_class_greeter(name)
  end
end

Animal.public_class_greeter('John') works fine, printing John greets private class method.

However, Animal.new.public_instance_greeter("John") throws an error: NoMethodError: private method 'private_class_greeter' called for Animal:Class.

That is expected, as the invocation self.class.private_class_greeter is same as Animal.private_class_greeter, which obviously throws an error.

After searching on how this can be fixed, I came up with the following code, that does the job:

class Animal
  class << self
    def public_class_greeter(name)
      private_class_greeter(name)
    end

  private
    def private_class_greeter(name)
      puts "#{name} greets private class method"
    end
  end

  define_method :public_instance_greeter, &method(:private_class_greeter)
end

I don't exactly understand what is happening here: &method(:private_class_greeter).

Could you please explain what does this mean?

If I were to replace:

define_method :public_instance_greeter, &method(:private_class_greeter)

with:

def public_instance_greeter
  XYZ
end

then, what should be the content in place of XYZ?

Satyaram B V
  • 307
  • 2
  • 10

2 Answers2

12

How does Ruby parse &method(:private_class_greeter)?

The expression &method(:private_class_greeter) is

  • the value of the method call method(:private_class_greeter)
  • prefixed with the & operator.

What does the method method do?

The method method looks up the specified method name in the current context and returns a Method object that represents it. Example in irb:

def foo
  "bar"
end

my_method = method(:foo)
#=> #<Method: Object#foo>

Once you have this method, you can do various things with it:

my_method.call
#=> "bar"

my_method.source_location   # gives you the file and line the method was defined on
#=> ["(irb)", 5]

# etc.

What is the & operator for?

The & operator is used to pass a Proc as a block to a method that expects a block to be passed to it. It also implicitly calls the to_proc method on the value you pass in, in order to convert values that are not Proc into a Proc.

The Method class implements to_proc — it returns the contents of the method as a Proc. Therefore, you can prefix a Method instance with & and pass it as a block to another method:

def call_block
  yield
end

call_block &my_method   # same as `call_block &my_method.to_proc`
#=> "bar"

The define_method method just happens to take a block with the contents of the new method that is being defined. In your example, &method(:private_class_greeter) passes in the existing private_class_greeter method as a block.


Is this how &:symbol works?

Yes. Symbol implements to_proc so that you can simplify your code like this:

["foo", "bar"].map(&:upcase)
#=> ["FOO", "BAR"]

# this is equivalent to:
["foo", "bar"].map { |item| item.upcase }

# because
:upcase.to_proc

# returns this proc:
Proc { |val| val.send(:upcase) }

How can I replicate &method(:private_class_greeter)?

You can pass in a block that calls the target method:

define_method :public_instance_greeter do |name|
  self.class.send(:private_class_greeter, name)
end

Of course, then you don't need to use define_method anymore, which results in the same solution Eric mentioned in his answer:

def public_instance_greeter(name)
  self.class.send(:private_class_greeter, name)
end
Mate Solymosi
  • 5,699
  • 23
  • 30
3

First, take good care with your indentation. private should be 2 spaces to the right: it gives the impression that public_instance_greeter is private otherwise.

If you don't care about encapsulation, you could simply use Kernel#send:

class Animal
  class << self
    def public_class_greeter(name)
      private_class_greeter(name)
    end

    private
    def private_class_greeter(name)
      puts "#{name} greets private class method"
    end
  end

  def public_instance_greeter(name)
    self.class.send(:private_class_greeter, name)
  end
end

Animal.public_class_greeter('John')
# John greets private class method
Animal.new.public_instance_greeter("John")
# John greets private class method
Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
  • 1
    Thanks for your suggestion about the indentation. I am aware of popular style guides. I think negative indenting `private` keyword helps to easily identify where private methods start. That is the reason why I prefer indenting the `private` keyword to root level. I know this is against most of the style guides. Do you think my thought process makes sense? – Satyaram B V Jul 10 '17 at 14:23
  • 1
    @SatyaramBV: If you (and your colleagues) can read it, that's fine for me. One way to avoid this problem would be to move the ` class << self` to the end of the `Animal` class. – Eric Duminil Jul 10 '17 at 14:26
  • 1
    @SatyaramBV FWIW, all the teams I've worked on have prefered the outdented accessibility keywords as well. – coreyward Jul 10 '17 at 15:43