In Ruby, Is There No Way to Dynamically Define a Local Variable in the Current Context

In Ruby, is there no way to dynamically define a local variable in the current context?

It seems that Ruby's magic would provide a way, but according to Matz, this was only possible in 1.8 via eval and only in certain contexts (i.e. irb). As of 1.9, this behavior was taken out ("strictly forbidden"):

Matz himself weighs in here: https://www.ruby-forum.com/topic/155673#685906

I read from somewhere that now Ruby can't dynamically create local variable. Is it true or just a bug?


The local variables are created in compile time, so that local
variables that are defined in eval() cannot be accessed outside of
eval. In 1.8, irb and tryruby does line by line compilation so that
local variables are spilled from eval(), but in 1.9, it's strictly
prohibited even under line-by-line compilation.

          matz.

(Non-sequitur alternative here, for anyone who wants something like this but not the exact technical situation that the questioner has):

Use a hash:

local_hash = {}

my_vars.each_pair do |k,v|
local_hash[k] = v
end

puts local_hash['foo']
#=> 'baz'

How to dynamically create a local variable?

You cannot dynamically create local variables in Ruby 1.9+ (you could in Ruby 1.8 via eval):

eval 'foo = "bar"'
foo # NameError: undefined local variable or method `foo' for main:Object

They can be used within the eval-ed code itself, though:

eval 'foo = "bar"; foo + "baz"'
#=> "barbaz"

Ruby 2.1 added local_variable_set, but that cannot create new local variables either:

binding.local_variable_set :foo, 'bar'
foo # NameError: undefined local variable or method `foo' for main:Object

This behavior cannot be changed without modifying Ruby itself. The alternative is to instead consider storing your data within another data structure, e.g. a Hash, instead of many local variables:

hash = {}
hash[:my_var] = :foo

Note that both eval and local_variable_set do allow reassigning an existing local variable:

foo = nil
eval 'foo = "bar"'
foo #=> "bar"
binding.local_variable_set :foo, 'baz'
foo #=> "baz"

How do I dynamically create a local variable in Ruby?

You have to use the correct binding. In IRB for example this would work:

irb(main):001:0> eval "t=2", IRB.conf[:MAIN_CONTEXT].workspace.binding
=> 2
irb(main):002:0> local_variables
=> [:t, :_]
irb(main):003:0> eval "t"
=> 2
irb(main):004:0> t
=> 2

Dynamically set local variables in Ruby

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.

How to create dynamic global and local variables?

Take this answer with a grain of salt because I'm no expert on Ruby's eval, but this worked for me:

require 'json'

hash = '{"$a":5,"b":10}'
hash1 = JSON.parse(hash)
bind = binding
hash1.each do |k,v|
# Dunno what this is for and things work fine without it.
# singleton_class.send(:attr_accessor,k)

# You don't want to call `send` here, just call `eval` directly.
eval("#{k}=#{v}", bind)
end

puts $a
# => 5

puts b
# => 10

Note that I'm passing in a binding from the parent scope to eval since you'll want to access local variables such as b in the parent scope after you exit the loop.

Lastly, v needed to be part of the string passed to eval, not the second parameter passed to eval, which, if anything, needs to be a binding. If you pass an integer it will raise an exception.

However, I should warn you that global variables should be used with restraint, and that using eval can be dangerous, especially if you get this JSON from someone else.

Dynamic variable from each method

As other commenters suggested, you may need a hash instead of a dynamic variable:

collection = ["s", "g", "l"]

collection.each do |key|
devan[key] = ['h1', 'h2', 'h3']
devan[key].each do |s1|
puts "from #{key} we got #{s1}"
end
end

With a hash, you get "for free" an arsenal of easy to use methods, which would otherwise be more cumbersome with dynamic variables. For example:

# Add an element to the array for the 's' key:
devan['s'].push 'h4'

# Add an element to the array for each key:
devan.each_key { |k| devan[k].push 'h5' }

# Print array length for each key:
devan.each { |k,v| puts v.length }

Using loop counter as part of a variable's name in Ruby

As others have pointed out it is not possible to create local variables dynamically in Ruby, you could set up a binding as well if you're looking for another method of achieving this.

With eval

b = binding
10.times do |i|
eval("var#{i} = 'foo'", b)
end

> eval("var1", b)
=> "foo"

> eval("local_variables", b)
=> [:var9, :var8, :var7, :var6, :var5, :var4, :var3, :var2, :var1, :var0, :b, :_]

Without eval

b = binding
10.times do |i|
b.local_variable_set("var#{i}", 'foo')
end

> b.local_variable_get('var1')
=> "foo"

> b.local_variables
=> [:var9, :var8, :var7, :var6, :var5, :var4, :var3, :var2, :var1, :var0, :b, :_]

Dynamic variable from string in Ruby

Make a hash of files:

readfile = File.open("myfile.txt", 'r')

files = {
'foo' => File.open("file1.txt", 'w'),
'bar' => File.open("file2.txt", 'w')
}

for line in readfile
files[line[0..2]].write(line)
end
files.each {|k, v| v.close}

Can you dynamically initialize multiple variables on one line in ruby?

There is a way, using eval, but you would rather not want to use it (and I would even go that far to say that it might be better not to learn it until well later).

There is simply no case when you would use that instead of plain arrays.

For your example, one should use class Range and method map:

(0..3).map{|i| i * 2}
#=> [0, 2, 4, 6]

You can see that this has been done without declaring any variable - even i is alive just within the block passed to map. It doesn't exist afterwards.



Related Topics



Leave a reply



Submit