Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't protected methods be called with symbol to proc?

Tags:

ruby

Given the following class:

class Foo
  def a
    dup.tap { |foo| foo.bar }
  end

  def b
    dup.tap(&:bar)
  end

  protected

  def bar
    puts 'bar'
  end
end

It seems like both Foo#a and Foo#b should be equivalent, but they're not:

> Foo.new.a
bar
 => #<Foo:0x007fe64a951ab8>

> Foo.new.b
NoMethodError: protected method `bar' called for #<Foo:0x007fe64a940a88>

Is there a reason for this? Is this a bug?

Tested on Ruby 2.2.3p173

like image 322
muirbot Avatar asked Dec 06 '15 22:12

muirbot


1 Answers

Let's start by noting that in Ruby, as you probably know, in a method a declared on a class Foo, I can invoke protected methods on any instance of Foo.

How does Ruby determine whether we're in a method declared on class Foo though? To understand this, we'll have to dig into the internals of method invocation. I'll be using examples from version 2.2 of MRI, but presumably the behavior and implementation are the same in other versions (I'd love to see results from testing this on JRuby or Rubinious, though).

Ruby does this in rb_call0. As the comment suggests, self is used to determine whether we can call protected methods. self is retrieved in rb_call from the current thread's call frame info. Then, in rb_method_call_status, we check that this value of self is of the same class on which a protected method is defined.

Blocks confuse the issue somewhat. Remember that local variables in a Ruby method are captured by any block declared in that method. This means that in the block, self is the same self on which the method was invoked. Let's look at an example:

class Foo
    def give_me_a_block!
        puts "making a block, self is #{self}"
        Proc.new do
            puts "self in block0 is #{self}"
       end
    end
end

proc = Foo.new.give_me_a_block!

proc.call

Running this, we see the same instance of Foo is the same at all levels, even though we called the proc from a completely different object.

So, now we understand why it's possible to call a protected method on another instance of the same class from within a block in a method.

Now let's look at why a proc created with &:bar can't do this. When we place an & sign before a method argument we do two things: instruct ruby to pass this argument as a block, and instruct it to call to_proc on it.

This means invoking the method Symbol#to_proc. This method is implemented in C, but when we invoke a C method, the pointer to self on the current frame becomes the receiver of that C method -- in this case, it becomes the symbol :bar. So we're looking at the instance of foo we got as though we are in a method of class Symbol, and we can't invoke a protected method.

That's a mouthful, but hopefully it makes enough sense. Let me know if you have any suggestions as to how I might improve it!

like image 126
jjm Avatar answered Oct 19 '22 09:10

jjm