Can I create a proc in the context of itself?

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

# ...