Since a proc is an object, can I create a proc in the scope of its own instance?
For example:
prc = Proc.new do
foo
end
def prc.foo
123
end
prc.call
# NameError: undefined local variable or method `foo' for main:Object
Either by changing self
or by having an explicit receiver
referring to the proc.
That receiver has to be evaluated dynamically, e.g. the following should work:
other_prc = prc.clone
def other_prc.foo
456
end
other_prc.call
#=> 456 <- not 123
Which means that I cannot just "hard-code" it via:
prc = Proc.new do
prc.foo
end
In other words: is there any way to refer to the procs instance from within the proc?
Another example without foo
: (what to put for # ???
)
prc = Proc.new do
# ???
end
prc == prc.call #=> true
other_prc = prc.clone
other_prc == other_prc.call #=> true
Replacing # ???
with prc
would only satisfy prc == prc.call
but not other_prc == other_prc.call
. (because other_prc.call
would still return prc
)
Disclaimer: I'm answering my own question
The solution is surprisingly simple. Just override call
to invoke the proc via instance_exec
:
Executes the given block within the context of the receiver (obj). In order to set the context, the variable
self
is set to obj while the code is executing, giving the code access to obj's instance variables. Arguments are passed as block parameters.
prc = proc { |arg|
@a ||= 0
@a += 1
p self: self, arg: arg, '@a': @a
}
def prc.call(*args)
instance_exec(*args, &self)
end
Here, the receiver is the proc itself and the "given block" is also the proc itself. instance_exec
will therefore invoke the proc in the context of its own instance. And it will even pass any additional arguments!
Using the above:
prc
#=> #<Proc:0x00007f84d29dcbb0>
prc.call(:foo)
#=> {:self=>#<Proc:0x00007f84d29dcbb0>, :arg=>:foo, :@a=>1}
# ^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^
# correct object passes args
prc.call(:bar)
#=> {:self=>#<Proc:0x00007f84d29dcbb0>, :arg=>:bar, :@a=>2}
# ^^^^^^
# preserves ivars
prc.instance_variable_get(:@a)
#=> 2 <- actually stores ivars in the proc instance
other_prc = prc.clone
#=> #<Proc:0x00007f84d29dc598>
# ^^^^^^^^^^^^^^^^^^
# different object
other_prc.call(:baz)
#=> {:self=>#<Proc:0x00007f84d29dc598>, :arg=>:baz, :@a=>3}
# ^^^^^^
# ivars are cloned
other_prc.call(:qux)
#=> {:self=>#<Proc:0x00007f84d29dc598>, :arg=>:qux, :@a=>4}
prc.call(:quux)
#=> {:self=>#<Proc:0x00007f84d29dcbb0>, :arg=>:quux, :@a=>3}
# ^^^^^^
# both instances have separate ivars
A general approach that is typically used in DSLs is referred to as the Clean Room pattern - an object you build for the purpose of evaluating blocks of DSL code. It is used to restrict the DSL from accessing undesired methods, as well as to define the underlying data the DSL works on.
The approach looks something like this:
# Using struct for simplicity.
# The clean room can be a full-blown class.
first_clean_room = Struct.new(:foo).new(123)
second_clean_room = Struct.new(:foo).new(321)
prc = Proc.new do
foo
end
first_clean_room.instance_exec(&prc)
# => 123
second_clean_room.instance_exec(&prc)
# => 321
It appears that what you are looking for is to have the Proc object itself serve as both the block and the clean room. This is a bit unusual, since the block of code is what you typically want to have reused on different underlying data. I suggest you consider first whether the original pattern might be a better fit for your application.
Nevertheless, having the Proc object serve as the clean room can indeed be done, and the code looks very similar to the pattern above (the code also looks similar to the approach you posted in your answer):
prc = Proc.new do
foo
end
other = prc.clone
# Define the attributes in each clean room
def prc.foo
123
end
def other.foo
321
end
prc.instance_exec(&prc)
# => 123
other.instance_exec(&other)
# => 321
You could also consider making the approach more convenient to run by creating a new class that inherits from Proc instead of overriding the native call
method. It's not wrong per-se to override it, but you might need the flexibility to attach it to a different receiver, so this approach lets you have both:
class CleanRoomProc < Proc
def run(*args)
instance_exec(*args, &self)
end
end
code = CleanRoomProc.new do
foo
end
prc = code.clone
other = code.clone
def prc.foo
123
end
def other.foo
321
end
prc.run
# => 123
other.run
# => 321
And if you cannot use a new class for some reason, e.g. you are getting a Proc object from a gem, you could consider extending the Proc object using a module:
module SelfCleanRoom
def run(*args)
instance_exec(*args, &self)
end
end
code = Proc.new do
foo
end
code.extend(SelfCleanRoom)
prc = code.clone
other = code.clone
# ...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With