One of the features of Ruby’s Kernel module (which is imported into Object, so it’s available in every class) is the binding. The binding allows you to keep a pointer to the scope at a particular location in your application and then later evaluate code from that location.
For example (and this came up as the result of a question on the ruby talk mailing list), one of the things you can do is obtain a list of the local variables of a method:
def my_vars(a)
b=a
c=b+a
puts local_variables
end
returns the three variables, a, b, and c. That’s fine, we’re still within the scope of the method. But how about outside of our method? This is where #binding and the Binding class come into play:
def outside(b)
puts eval("local_variables", b)
end
def my_vars(a)
b=a
c=b+a
outside(binding)
end
One thing to note: in order to work with the binding, we have to #eval code within its scope. Thus the call to #eval with the method we are invoking and the binding. This leads to the question from the mailing list:
we already have #has_block? to see if a block was passed. So how about
a #has_arguments? to query if _any_ arguments have been passed — Intransition
So how would we go about writing a #has_arguments? — here is a first stab:
class Object
def has_arguments?(b)
vars = eval("local_variables",b)
return false if vars.length == 0
vars.each do |v|
return false if eval("#{v}.nil?",b)
end
return true
end
end
class Foo
def initialize(first=nil)
if (has_arguments? binding)
puts "We're defined"
else
puts "Not!"
end
end
end
The only real drawback here is that we’ve got to pass in the binding itself. Let’s try again:
class Object
def has_arguments?(&b)
vars = eval("local_variables",b.binding)
return false if vars.length == 0
vars.each do |v|
return false if eval("#{v}.nil?",b)
end
return true
end
end
class Foo
def initialize(first=nil)
if (has_arguments? {}) # note the empty block
puts "We're defined"
else
puts "Not!"
end
end
end
In this case, instead of explicitly calling #binding, we’re passing in an empty block — the block has it’s own scope, but it happens to contain the scope of then enclosing method. It’s still not perfect, however:
- You need to pass in an empty block.
- It fails if you have local variables other than those passed in as arguments. (Pointed out by Joel VanderWerf)
Ok, it’s got some serious flaws (Charles Nutter posted a different way of solving the problem, which does work for all cases, and doesn’t use #binding). But it does give an idea of what you can do with #binding.
The final example doesn’t work on Ruby 1.9.1, line 6 can be changed to:
return false if eval(“#{v}.nil?”,b.binding)
and it will work with both Ruby 1.8.6 and 1.9.1.