Making a method available in a block without changing its context?

I would like to create a class that does the following:

  • Its instance takes a block.
  • During the initialization of an instance, it performs certain actions, then calls the block, then performs more actions.
  • Another method from this class must be available inside the block.

This is how I want it to work:

Foo.new do
  puts "Hell, I can get you a toe by 3 o'clock this afternoon..."
  bar
  puts "...with nail polish."
end

      

I managed to achieve this with the following class:

class Foo

  def initialize(&block)

    puts "This represents a beginning action"

    instance_eval &block

    puts "This symbolizes an ending action"

  end

  def bar
    puts "I should be available within the block."
  end

end

      

As you can see, I am using a trick instance_eval

. It allows you to use it bar

inside a block.

It works great, but the problem here is what instance_eval

makes the current local context unavailable. If I use it from another class, I lose access to the methods of that class. For example:

class Baz
  def initialize
    Foo.new do
      bar  # -> Works
      quux # -> Fails with "no such method"
    end
  end

  def quux
    puts "Quux"
  end
end

      

The question arises: how to allow execution bar

inside the block without losing access to quux

?

The only way my newbie comes up with is to pass bar

as an argument to a block. But this requires more typing, so I would like to aviod if possible.

+3


source to share


2 answers


instance_eval

does not take into account the scope where the block is called, so each method call only applies to what is defined inside Foo.

So you have 2 options. Or

def initialize
  baz = self
  Foo.new do
    bar  # -> Works
    baz.quux # -> Works
  end
end

      



or

def initialize
  puts "This represents a beginning action"
  yield self
  puts "This symbolizes an ending action"
end

....

def initialize
  Foo.new do |b|
    b.bar  # -> Works too
    quux # -> Works too
  end
end

      

I'm not sure which one would be the best, but the choice you choose is based on your own preference.

+3


source


It works great, but the problem here is that instance_eval makes the current local context unavailable

instance_eval () doesn't do this. The code inside all blocks, that is, something similar:

{ code here }

      

can see the variables that existed in the surrounding area at the time the block was created. The block cannot see variables in the surrounding space while the EXECUTED block is being executed. In information jargon, a block is known as a closure because it "closes" variables in the surrounding scope at creation time.

What instance_eval does is assign a new value to the variable self, which blocks the block. Here's an example:

puts self  #=>main
func = Proc.new {puts self}  
func.call  #=>main

class Dog
  def do_stuff(f)
    puts self
    f.call
  end
end

d = Dog.new
d.do_stuff(func) 

--output:--
#<Dog:0x000001019325b8>
main   #The block still sees self=main because self was equal to main when the block was created and nothing changed the value of that self variable

      

Now with an instance_eval instance:

class Dog
  def do_stuff(f)
    puts self
    instance_eval &f
  end
end

d = Dog.new
d.do_stuff(func) 

--output:--
#<Dog:0x000001011425b0>
#<Dog:0x000001011425b0>  #instance_eval() changed the value of a variable called self that the block `closed over` at the time the block was created

      

You also need to understand that when you call a method and you do not specify a "receiver", for example

quux()

      

... then ruby ​​converts that string to:

self.quux()

      

So, it's important to know the value of self. Examine this code:

class Dog
  def do_stuff(f)
    puts self  #Dog_instance
    instance_eval &f  #equivalent to self.instance_val &f, 
                      #which is equivalent to Dog_instance.instance_eval &f
  end
end

      

Since instance_eval () sets the value of the self variable inside the instance_eval () 'receiver' block, the value of self inside the block is set to Dog_instance.

Examine your code here:



puts self #=> main

Foo.new do
  puts self  #=>main

  bar  #equivalent to self.bar--and self is not a Foo or Baz instance
       #so self cannot call methods in those classes  
end

      

Examine your code here:



class Foo
  def initialize(&block)
    instance_eval &block  #equivalent to self.instance_eval &block
  end
end

      

And inside Foo # initialize () self is equal to the new instance of Foo. This means that inside the block, self is set equal to the instance of Foo, and therefore if you write inside the block:

quux()

      

This is equivalent to:

self.quux()

      

which is equivalent to:

Foo_instance.quux()

      

which means that quux () must be defined in Foo.

In this answer:

class Baz
  def initialize
    puts self  #=>Baz_instance

    baz = self

    Foo.new do
      bar  # -> Works
      baz.quux # -> Works
    end

  end

  def quux
    puts "Quux"
  end
end

b = Baz.new

      

... the lines bar and baz seem to have the same destinations:

   puts self  #=>Baz_instance

   baz = self  #To evaluate that assignment ruby has to replace the variable self 
               #with its current value, so this is equivalent to baz = Baz_instance
               #and baz no longer has any connection to a variable called self.

   Foo.new do
      bar  #=> equivalent to self.bar, which is equivalent to Baz_instance.bar
      baz.quux  #=> equivalent to Baz_instance.quux
    end

      

But when instance_eval () executes that block, which is everything between do and end, instance_eval () changes the value of self:

   Foo.new do #instance_eval changes self inside the block so that self = Foo_instance
      bar  #=> equivalent to self.bar which is now equivalent to Foo_instance.bar
      baz.quux  #=> the block still sees baz = Baz_instance, so equivalent to Baz_instance.bar
    end

      

+3


source







All Articles