Reputation: 114138
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
)
Upvotes: 10
Views: 1736
Reputation: 857
The second Attempt Edited after comment
# This solution has a limit you have to return the `Proc` itself
with_proc = proc do |aproc, others|
aproc.instance_variable_set(:@a, aproc.instance_variable_get(:@a) || 0)
aproc.instance_variable_set(:@a, aproc.instance_variable_get(:@a) + 1)
p self: aproc, arg: others, '@a': aproc.instance_variable_get(:@a)
aproc
end
prc = with_proc.(with_proc, :foo)
# => {:self=>#<Proc:0x000055864be1a740@pro_self.rb:1>, :arg=>:foo, :@a=>1}
puts "prc: #{prc}"
puts "prc.equal?(with_proc): #{prc.equal?(with_proc)}"
# => prc: #<Proc:0x000055864be1a740@pro_self.rb:1>
# => prc.equal?(with_proc): true
prc.call(prc, :bar)
puts "prc @a: #{prc.instance_variable_get(:@a)}"
# => {:self=>#<Proc:0x000055864be1a740@pro_self.rb:1>, :arg=>:bar, :@a=>2}
# => prc @a: 2
other_prc = prc.call(prc.clone, :baz)
puts "other_prc: #{other_prc}"
# => {:self=>#<Proc:0x000055864be1a0b0@pro_self.rb:1>, :arg=>:baz, :@a=>3}
# => other_prc: #<Proc:0x000055864be1a0b0@pro_self.rb:1>
other_prc.call(other_prc, :qux)
#=> {:self=>#<Proc:0x000055864be1a0b0@pro_self.rb:1>, :arg=>:qux, :@a=>4}
prc.call(prc, :quux)
# => {:self=>#<Proc:0x000055864be1a740@pro_self.rb:1>, :arg=>:quux, :@a=>3}
With this solution you can return whatever is necessary
prc = proc do |ref_to_self, others|
self_reference = ref_to_self.instance_variable_get(:@ident)
self_reference.instance_variable_set(:@a, self_reference.instance_variable_get(:@a) || 0)
self_reference.instance_variable_set(:@a, self_reference.instance_variable_get(:@a) + 1)
p ({self: self_reference.instance_variable_get(:@ident),
arg: others,
'@a': self_reference.instance_variable_get(:@a)})
end
prc.instance_variable_set(:@ident, prc)
prc.call(prc, :foo)
puts "prc: #{prc}"
prc.call(prc, :bar)
puts "prc @a: #{prc.instance_variable_get(:@a)}"
other_prc = prc.clone
other_prc.instance_variable_set(:@ident, other_prc)
other_prc.call(other_prc, :baz)
puts "other_prc: #{other_prc}"
other_prc.call(other_prc, :qux)
prc.call(prc, :quux)
# {:self=>#<Proc:0x00005559f1f6d808@pro_self.rb:71>, :arg=>:foo, :@a=>1}
# prc: #<Proc:0x00005559f1f6d808@pro_self.rb:71>
# {:self=>#<Proc:0x00005559f1f6d808@pro_self.rb:71>, :arg=>:bar, :@a=>2}
# prc @a: 2
# {:self=>#<Proc:0x00005559f1f6d1f0@pro_self.rb:71>, :arg=>:baz, :@a=>3}
# other_prc: #<Proc:0x00005559f1f6d1f0@pro_self.rb:71>
# {:self=>#<Proc:0x00005559f1f6d1f0@pro_self.rb:71>, :arg=>:qux, :@a=>4}
# {:self=>#<Proc:0x00005559f1f6d808@pro_self.rb:71>, :arg=>:quux, :@a=>3}
First Attempt
Edited after comment. That I know there is not a direct way to reference a Proc
object inside the block you pass to new
I tried to get closer to your code using tap.
I hope this can help
def proc_reference_to_self(a_proc)
first = Proc.new do
puts "Hello"
end.tap(&a_proc)
end
second_prc = Proc.new do |first|
p first
first.call
puts "second_prc"
p second_prc
end
# This execute second_prc as a block
proc_reference_to_self(second_prc)
# first and second are different objects but you can still reference first
# inside second_proc
# <Proc:0x000055603a8c72e8@ruby_array_of_paths.rb:75>
# Hello
# second_prc
# <Proc:0x000055603a8c7338@ruby_array_of_paths.rb:81>
Upvotes: 4
Reputation: 3245
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
# ...
Upvotes: 5
Reputation: 114138
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
Upvotes: 7
Reputation: 16667
Ok, now I think I understand what you mean. As I mentioned in the comments, it can be done by nesting closures. Because Procs/lambdas are anonymous, the closure nesting provides a way for the lambda to receive a dynamic reference to itself, thereby allowing it to instance_eval
code in the context of self
.
wrapped_dispatch = ->(f) { f[f] }
proc_wrapped = lambda do |myself|
lambda do |n|
myself.instance_eval do
# in context of self
bar(n)
end
end
end
def proc_wrapped.bar(n)
p "bar #{n}"
end
wrapped_dispatch[proc_wrapped].call(123)
# => "bar 123"
# can also save it "unwrapped"
prc = wrapped_dispatch[proc_wrapped]
prc.call(123)
# => "bar 123"
# Very late binding to dynamic receiver
def proc_wrapped.bar(n)
p "BAR #{n}"
end
prc.call(123)
# => "BAR 123"
# and if the "wrapped-ness" bothers you, link them together and delegate
proc_wrapped.define_singleton_method(:call) do |n|
prc.call(n)
end
def proc_wrapped.bar(n)
p "BBBBAAAARRRRR"
end
proc_wrapped.call(123)
# => "BBBBAAAARRRRR"
other_proc_wrapped = proc_wrapped.clone
wrapped_dispatch[other_proc_wrapped].call(123)
# => "BBBBAAAARRRRR"
def other_proc_wrapped.bar(n)
p "foo #{n}"
end
wrapped_dispatch[other_proc_wrapped].call(123)
# => "foo 123"
proc_wrapped.call(123)
# => "BBBBAAAARRRRR"
I'm noticing this behavior is very similar to a class's instances (Foo.new
) vs a class's singleton class (Foo.singleton_class
), which makes sense since closures and objects are equivalent. This means if you really want behavior like this, you ought to just use a class, its singleton class, and its instances, as is idiomatic in Ruby.
Upvotes: 3
Reputation: 84343
If I understand your question correctly, leveraging the outer scope of the closure may do what you want. This is admittedly a very contrived example that registers your nested Proc objects in an Array. The second Proc isn't created until the first one is called, but they both retain their bindings to the outer scope.
@procs = []
@foo = 1
@procs << proc do
# Don't keep re-registering the nested Proc on
# subsequent invocations.
@procs << proc { @foo + 1 } unless @procs.count == 2
@foo
end
@procs.map &:call
#=> [1, 2]
@foo = 3
@procs.map &:call
#=> [3, 4]
Upvotes: 4