Stefan
Stefan

Reputation: 114138

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)

Upvotes: 10

Views: 1736

Answers (5)

Giuseppe Schembri
Giuseppe Schembri

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

AmitA
AmitA

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

Stefan
Stefan

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

Kache
Kache

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

Todd A. Jacobs
Todd A. Jacobs

Reputation: 84343

Leverage the Outer Scope of the Closures

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

Related Questions