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.
source to share
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.
source to share
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
source to share