Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create blank binding in the scope of an object

Tags:

ruby

eval

class Foo
  def self.run(n,code)
    foo = self.new(n)
    @env = foo.instance_eval{ binding }
    @env.eval(code)
  end
  def initialize(n)
    @n = n
  end
end

Foo.run( 42, "p @n, defined? foo" )
#=> 42
#=> "local-variable"

The sample program above is intended to evaluate arbitrary code within the scope of a Foo instance. It does that, but the binding is "polluted" with the local variables from the code method. I don't want foo, n, or code to be visible to the eval'd code. The desired output is:

#=> 42
#=> nil

How can I create a binding that is (a) in the scope of the object instance, but (b) devoid of any local variables?


The reason that I am creating a binding instead of just using instance_eval(code) is that in the real usage I need to keep the binding around for later usage, to preserve the local variables created in it.

like image 271
Phrogz Avatar asked Nov 08 '14 02:11

Phrogz


2 Answers

so like this? or did i miss something important here?

class Foo
  attr_reader :b

  def initialize(n)
    @n = n
    @b = binding
  end

  def self.run(n, code)
    foo  = self.new(n)
    foo.b.eval(code)
  end
end

Foo.run(42, "p @n, defined?(foo)")
# 42
# nil

or move it further down to have even less context

class Foo
  def initialize(n)
    @n = n
  end

  def b
    @b ||= binding
  end

  def self.run(n, code)
    foo  = self.new(n)
    foo.b.eval(code)
  end
end

Foo.run(42, "p @n, defined?(foo), defined?(n)")
# 42
# nil
# nil
like image 135
phoet Avatar answered Oct 16 '22 19:10

phoet


Answer:

module BlankBinding
  def self.for(object)
    @object = object
    create
  end
  def self.create
    @object.instance_eval{ binding }
  end
end

Description:

In order to get a binding with no local variables, you must call binding in a scope without any of them. Calling a method resets the local variables, so we need to do that. However, if we do something like this:

def blank_binding_for(obj)
  obj.instance_eval{ binding }
end

…the resulting binding will have an obj local variable. You can hide this fact like so:

def blank_binding_for(_)
  _.instance_eval{ binding }.tap{ |b| b.eval("_=nil") }
end

…but this only removes the value of the local variable. (There is no remove_local_variable method in Ruby currently.) This is sufficient if you are going to use the binding in a place like IRB or ripl where the _ variable is set after every evaluation, and thus will run over your shadow.

However, as shown in the answer at top, there's another way to pass a value to a method, and that's through an instance variable (or class variable, or global variable). Since we are using instance_eval to shift the self to our object, any instance variables we create in order to invoke the method will not be available in the binding.

like image 42
Phrogz Avatar answered Oct 16 '22 20:10

Phrogz