Ruby binding instance_eval access to local vars?

Viewed 73

Working on some functionality where I need to be able to define methods and local vars from strings via a binding

I think the code speaks best for itself, so here is what I'm trying to accomplish:

mybind = binding

mybind.eval("a = 5")
mybind.eval("a") #=> 5

a #=> NameError: undefined local variable or method `a'...
# Calling it in global/main scope doesn't find anything, but it's found in the `mybind` scope

mybind.eval("def thing; 3; end")
mybind.eval("thing") #=> 3

thing #=> NameError: undefined local variable or method `thing'...
# Calling it in global/main scope doesn't find anything, but it's found in the `mybind` scope

Essentially, I don't want any methods/vars defined to go to the global scope, but I want them to be callable within a second call to the same binding/eval. These will come through as strings, so callable blocks is out of the question as well.

I've tried instance_eval, class_eval, eval, and instance_exec

instance_eval is ALMOST perfect, except for some reason any local vars defined cannot be accessed within the next instance_eval called.

I'm a bit new to using binding, so if there is something obvious I'm missing, please let me know. Otherwise if there is anybody that has any idea on how to accomplish this, even with a different flow I would really appreciate any help.

Thanks!

I'm on Ruby 2.7.2, but can upgrade if that will provide a solution.

1 Answers

binding, by its design, gets the current binding whenever it's called. If you call it in the global scope, you'll get the global binding. What it sounds like you want is a binding that's not accessible to the user. We can create one of those easily, simply by calling binding inside of an otherwise empty function.

def new_binding
  binding
end

Now, every time I call new_binding, I get a fresh scope that inherits lexically from Ruby's standard global scope but that nobody else can access without that object.

irb(main):012:0> new_binding == new_binding
=> false
irb(main):013:0> my_binding = new_binding
=> #<Binding:0x00005562fd103a00>
irb(main):014:0> my_binding.eval("a = 1")
=> 1
irb(main):015:0> my_binding.eval("a")
=> 1

--

Edit after your comment

We can get a little closer to what you're looking for by wrapping new_binding in a trivial eigenclass.

def new_binding
  class <<Object.new
    return binding
  end
end

Now you can define methods, but you have to do so with an explicit self.

new_binding.eval("def self.thing; 3; end")
new_binding.eval("thing")

It's very possible there's some trickery that can be done with method_added to get around the explicit receiver, but for whatever reason it seems like method_added doesn't want to work on eigenclasses. I might end up opening a new SO question about that myself to be honest, as the behavior is quite odd.

Related