33

I'm interested in dynamically setting local variables in Ruby. Not creating methods, constants, or instance variables.

So something like:

args[:a] = 1
args.each_pair do |k,v|
  Object.make_instance_var k,v
end
puts a
> 1

I want locally variables specifically because the method in question lives in a model and I dont want to pollute the global or object space.

5
  • 3
    Is there any particular reason you need local variables instead of simply accessing the pairs in args? Commented Feb 10, 2011 at 23:27
  • Do you mean that you have a bunch of variable names and values in method X which are then to be used in method Y as variables local to method Y ? Also, here is a useful comment from matz: ruby-forum.com/topic/155673#685906 Commented Feb 11, 2011 at 1:10
  • Basically I have a set of functions with nebulous parameters, and it would be a lot clearer to pass in: present_value(:n=>6,:m=>4..etc), and then use them in the function as local variables automatically instead of reduplicating myself by redefining them or using them in hash form. Along the lines of your comment Zabba I looked at: github.com/maca/arguments ...but couldnt get it to work reliably in 1.9.2 since its apparently no longer under active development. Commented Feb 12, 2011 at 17:22
  • Hehe, it doesn't work for 1.9.2, we lost the facitilities for extracting s trees from live objects. you could do crazy stuff with that. I am the author Commented May 16, 2012 at 5:51
  • Ruby 2.2.2 may add the ability to add this via binding.local_variables Commented Apr 27, 2015 at 20:08

5 Answers 5

31

As an additional information for future readers, starting from ruby 2.1.0 you can using binding.local_variable_get and binding.local_variable_set:

def foo
  a = 1
  b = binding
  b.local_variable_set(:a, 2) # set existing local variable `a'
  b.local_variable_set(:c, 3) # create new local variable `c'
                              # `c' exists only in binding.
  b.local_variable_get(:a) #=> 2
  b.local_variable_get(:c) #=> 3
  p a #=> 2
  p c #=> NameError
end

As stated in the doc, it is a similar behavior to

binding.eval("#{symbol} = #{obj}")
binding.eval("#{symbol}")
Sign up to request clarification or add additional context in comments.

2 Comments

Your example doesn't work as-is, because b.local_variable_set(:b, 3) overwrites the binding you previously stored in b. Storing the binding in a different variable fixes the problem.
@Kelvin thanks, I fixed it
15

The problem here is that the block inside each_pair has a different scope. Any local variables assigned therein will only be accessible therein. For instance, this:

args = {}
args[:a] = 1
args[:b] = 2

args.each_pair do |k,v|
  key = k.to_s
  eval('key = v')
  eval('puts key')
end

puts a

Produces this:

1
2
undefined local variable or method `a' for main:Object (NameError)

In order to get around this, you could create a local hash, assign keys to this hash, and access them there, like so:

args = {}
args[:a] = 1
args[:b] = 2

localHash = {}
args.each_pair do |k,v|
  key = k.to_s
  localHash[key] = v
end

puts localHash['a']
puts localHash['b']

Of course, in this example, it's merely copying the original hash with strings for keys. I'm assuming that the actual use-case, though, is more complex.

3 Comments

You're correct. Essentially I want a painless way to pass named arguments, and then use them in the method. Too bad the eval trick doesn't get it, I imagine any option would have miserable performance anyway.
key = k.to_s followed by eval('key = v') does not have the effect you expect, key = 1234 has the same effect
eval is not ideal because not everything can be easily serialized to string
10

interesting, you can change a local variable but you cannot set it:

def test
  x=3
  eval('x=7;')
  puts x
end

test => 7

def test
  eval('x=7;')
  puts x
end

test => NameError: undefined local variable or method `x' for main:Object

This is the only reason why Dorkus Prime's code works.

Comments

6

I suggest you use the hash (but keep reading for other alternatives).

Why?

Allowing arbitrary named arguments makes for extremely unstable code.

Let's say you have a method foo that you want to accept these theoretical named arguments.

Scenarios:

  1. The called method (foo) needs to call a private method (let's call it bar) that takes no arguments. If you pass an argument to foo that you wanted to be stored in local variable bar, it will mask the bar method. The workaround is to have explicit parentheses when calling bar.

  2. Let's say foo's code assigns a local variable. But then the caller decides to pass in an arg with the same name as that local variable. The assign will clobber the argument.

Basically, a method's caller must never be able to alter the logic of the method.

Alternatives

An alternate middle ground involves OpenStruct. It's less typing than using a hash.

require 'ostruct'
os = OpenStruct.new(:a => 1, :b => 2)
os.a  # => 1
os.a = 2  # settable
os.foo  # => nil

Note that OpenStruct allows you access non-existent members - it'll return nil. If you want a stricter version, use Struct instead.

This creates an anonymous class, then instantiates the class.

h = {:a=>1, :b=>2}
obj = Struct.new(* h.keys).new(* h.values)
obj.a  # => 1
obj.a = 2  # settable
obj.foo  # NoMethodError

Comments

1

since you don't want constants

args = {}
args[:a] = 1
args[:b] = 2

args.each_pair{|k,v|eval "@#{k}=#{v};"}

puts @b

2

you might find this approach interesting ( evaluate the variables in the right context)

fn="b*b"
vars=""
args.each_pair{|k,v|vars+="#{k}=#{v};"}
eval vars + fn

4

1 Comment

I'm not downvoting, but OP says instance variables are not acceptable. Which is reasonable, because that will alter the state of the object.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.