Feature #7747

Expanded API for Binding semantics

Added by Joshua Ballanco about 1 year ago. Updated about 1 month ago.

Target version:-


Currently, the only way to create a new instance of Binding is to get a copy of the current scope's binding, or ask some other object for its binding. In either case, the binding object returned always has semantics identical to the original binding. In other words, a binding object used with eval is capable of (not necessarily an exhaustive list):

  • redefining methods on the binding target
  • defining new methods on the binding target
  • getting/setting instance variables
  • getting/creating new constants

This feature proposal would introduce a new mechanism for creating a binding and adjusting its relationship with the source binding. For example, if you have a class (({Foo})) defined like so:

class Foo
  def say
    puts "hello!"

Then you would be able to create a new binding that ((won't)) propagate new definitions to the parent binding like so:

new_binding = Binding.new(Foo)
new_binding.propagate = false
new_binding.eval("def shout; puts 'HI!'; end")

Foo.new.say #=> "hello!"
Foo.new.shout #=> No Method Error
new_binding.eval("Foo.new.say") #=> "hello"
new_binding.eval("Foo.new.shout") #=> "HI!"

If, additionally, we introduce a way to merge or combine bindings, then this API could actually be used to implement refinements in pure Ruby like so (note I am also assuming that (({Binding#eval})) gains the ability to take a block):

class Module
  def refine(klass, &block)
    @refined_bindings ||= {}
    refinement_binding = Binding.new(klass)
    refinement_binding.propagate = false
    refinement_binding.shadow = true
    refinement_binding.eval &block
    (@refined_bindings[self.name] ||= []) << refinement_binding

  def using(mod)
    @refined_bindings[mod].each { |refinement| self.binding.merge(refinement) }

Following is the preliminary list of additional APIs I am tentatively proposing (though I expect this to change with additional discussion):

  • (({Binding.new(anobject)})) - creates a new Binding object that "inherits" the binding of the argument; essentially equivalent to (({anobject.send(:binding).dup}))
  • (({Binding#propagate}))/(({Binding#propagate=})) - boolean; determines whether new method, class, or Constant defs are propagated into the "parent" binding's scope
  • (({Binding#shadow}))/(({Binding#shadow=})) - boolean; sets whether or not new values for existing methods/constants/variables can be set
  • (({Binding#freeze})) - causes the Binding to capture all existing methods/constants/variables in scope at call time, and "disconnect" from the parent binding, so that any updates to definitions or values are no longer reflected into the "child" binding
  • (({Binding#merge(otherbinding)})) - combines the method/constant/variable bindings in (({otherbinding})) with the receiver; effectively shadows the "parent" of (({other_binding})) within the receiver binding =end


#1 Updated by Joshua Ballanco about 1 year ago

It should also be possible to implement feature request #6710 with this API. The "isolated" binding mentioned in that request could be implemented as:

def isolated_eval(src)
  isolated = Binding.new #=> No source binding creates an empty environment


Also available in: Atom PDF