Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I create a proc in the context of itself?

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)

like image 904
Stefan Avatar asked Jul 05 '20 08:07

Stefan


Video Answer


2 Answers

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
like image 189
Stefan Avatar answered Sep 30 '22 05:09

Stefan


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

# ...
like image 21
AmitA Avatar answered Sep 30 '22 03:09

AmitA