New to Ruby so apologies in advance if this is a dumb question. I have a need to supply a value to a block but would like not to update everywhere called.
Let's say I have the following block:
{ base.call_my_test }
It get's passed to a function where I'd like to wrap it in another block but supply that other blocks value to it such as:
def test(&block)
MyTest.with_base_object do |base|
#neither of these work
block.instance_variable_set(:@base, base) # Doesn't work
block.define_singleton_method(:hi) do
return base
end
block.call <---- Is it possible to define base here?
end
end
In the above example, is it possible to define base
on the block without passing as an argument? I tried instance_variable_set
as well as define_singleton_method
but I can't seem to get anything to be defined as it always says base
is undefined.
Not sure if this is even possible but figured I'd ask.
CodePudding user response:
First, blocks in Ruby are non-rigid Proc
objects. That means that you can pass extra arguments, and anyone who doesn't expect arguments will silently ignore them.
def test(&block)
base = Object.new
block.call(base)
end
test { |x| puts "Works with an object #{x}." }
test { puts "Works without an object as well!" }
So if you control the block where you need base
to be defined, then you can simply add the argument. Any other uses of the test
function will simply ignore the extra argument, even if they're not declared to take one.
But this is assuming you control any of the blocks at all. It sounds like you have some blocks somewhere out there that you can't change, and you automagically want to inject the name base
into their scope. You can change the class instance that a block is being invoked for by passing the block to instance_eval
.
Buyer beware: This will completely change self
. If the block in question tries to view self
or call any other methods on the object (it thinks) it's enclosed in, it will fail.
class HasBase
attr_reader :base
def initialize(base)
@base = base
end
end
def test(&block)
base = Object.new
HasBase.new(base).instance_eval(&block)
end
test { puts "I can see my #{base} from here!" }
Depending on your specific needs, you can start augmenting this to work with whatever your callers need. For instance, you can write method_missing
to fall back to the proc's original binding. But that's getting into some deep magic, and you probably don't want to play with that footgun in production for very long.