Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically set local variables in Ruby [duplicate]

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.

like image 390
Allyl Isocyanate Avatar asked Feb 10 '11 22:02

Allyl Isocyanate


4 Answers

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}")
like image 153
Geoffroy Avatar answered Nov 02 '22 04:11

Geoffroy


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.

like image 21
Dorkus Prime Avatar answered Nov 02 '22 06:11

Dorkus Prime


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

like image 29
Anno2001 Avatar answered Nov 02 '22 06:11

Anno2001


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
like image 6
Kelvin Avatar answered Nov 02 '22 05:11

Kelvin