Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What does `&method(:method_ name)` mean in ruby?

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?

like image 888
Satyaram B V Avatar asked Jul 10 '17 13:07

Satyaram B V


2 Answers

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
like image 156
Mate Solymosi Avatar answered Oct 21 '22 15:10

Mate Solymosi


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
like image 20
Eric Duminil Avatar answered Oct 21 '22 16:10

Eric Duminil