Access Variables Programmatically by Name in Ruby

Access variables programmatically by name in Ruby

What if you turn your problem around? Instead of trying to get names from variables, get the variables from the names:

["foo", "goo", "bar"].each { |param_name|
param = eval(param_name)
if param.class != Array
puts "#{param_name} wasn't an Array. It was a/an #{param.class}"
return "Error: #{param_name} wasn't an Array"
end
}

If there were a chance of one the variables not being defined at all (as opposed to not being an array), you would want to add "rescue nil" to the end of the "param = ..." line to keep the eval from throwing an exception...

Get The Name Of A Local Variable

foo = 1
bar = "42"
baz = Hash.new

%w(foo bar baz).each do |vn|
v = eval(vn)
puts "#{vn} = (#{v.class}) #{v}"
end

But this, of course, doesn't help you if you want a method with 1 argument.

Calling variable name of instance from within class

Unconventional method (solves the question you posed)

Okay. For the sake of posterity I'll put the code here on how to accomplish what you asked. Remember this is NOT the way the language was meant to be used. But if you get into meta-programming this will be very useful knowledge.

class Playlist

def display
puts "Your playlist name is #{name}"
end

private
def name
scope = ObjectSpace.each_object(Binding).to_a[-1]
scope.
local_variables.
select {|i| eval(i.to_s, scope) == self}.
map(&:to_s).delete_if {|i| i== "_"}.first
end

end

alternative = Playlist.new
# => #<Playlist:0x00000002caad08>
alternative.display
# Your playlist name is alternative

Details (how it works)

Alright let me explain the parts. ObjectSpace is where all objects get stored. You can see how many Objects exist by calling ObjectSpace.count_objects. The most useful feature, in my opinion, is the each_object method. With this method you can iterate over however many of any particular object which have been created. So for playlist you can call ObjectSpace.each_object(Playlist) and you get an Enumerable object. We can simply turn that into a list by appending .to_a on the end. But at this point you're getting the instances of Playlist in an Array like this: [#<Playlist:0x0000000926e540>, #<Playlist:0x000000092f4410>, #<Playlist:0x000000092f7d90>]. This is functional if you wanted to access them individually and perform some action. But this is not what you're trying to do since we don't have the instantiated variable name these instances are assigned to.

What we really want to call is the local_variables method and we want to call that in the main scope (not from within your classes scope). If you call local_variables from within your display method you get back an empty Array []. But if you call it in the main console after you've created an instance you would get back something like this [:alternative, :_]. Now we're talking! Now there's the issue of getting the scope from outside the class to be used within it. This was tricky to track down. Normally you could just pass in binding as a parameter, or even use TOPLEVEL_BINDING. But something I noticed showed me that these each create an instance of Binding that won't get updated any more. That means once you call TOPLEVEL_BINDING anything else you define, like another playlist, won't be updated and in your list of TOPLEVEL_BINDING.local_variables. This was a sad thing for me to find. But I discovered a way to solve this.

By calling ObjectSpace.each_object(Binding).to_a we now have a list of every binding instance. So we just need to know how to get the latest one that's up to date. After experimenting I found the last one will always be up to date. So we index by [-1]. Now we can call .local_variables on it and we will always get the latest collection of instance variables within the global scope. This is great! Now we just need to match the instance variable to the current Playlist that we're in. So we select from the global local_variables any that match the current instance. We need to call eval to get the instance, and with eval we need to tell it what scope to run in so we use select {|i| eval(i.to_s, scope) == self}. From there we take the symbols and map them to strings with .map(&:to_s) and lastly we have an extra item in our list we don't need. The underscore symbol is kind of a Ruby trick to get the last thing that was processed. So we'll need to remove it since it evaluated to the same id as our current variable instance did. So we do .delete_if {|i| i== "_"}. And lastly it's a list of one item, the thing we want, so we pick it out with .first

NOTE: This scope selecting method doesn't work in Rails. There are many bindings instantiated. The last one and the largest one with local_variables aren't the up to date ones.

This went through many unconventional means to accomplish the task you asked about. Now it may be you didn't know the standard way that something like naming a playlist class is done, and that's okay. No one knew at first, it is a learned trait.

Convential way to name a playlist

This is the preferred method for naming a playlist class.

class Playlist

def initialize(name)
@name = name
end

def display
puts "Your playlist name is #{@name}"
end

end

list = Playlist.new("Alternative")
list.display
# => "Your playlist name is Alternative"

This is rather straight forward. It's best to work with the way a language was designed to be used.

If I were you I would make list an Array of Playlist items and use it like this.

list = []
list << Playlist.new("Alternative")
list << Playlist.new("Rock")
list
# => [#<Playlist:0x000000028a4f60 @name="Alternative">, #<Playlist:0x000000028e4868 @name="Rock">]
list[0].display
# Your playlist name is Alternative
list[1].display
# Your playlist name is Rock

And now you have a list of playlists! Sweet!

When you get into meta-programming you may use a lot of features from the unconventional method here. meta-programming is where code writes code. It's fun!

Get the value of an instance variable given its name

The most idiomatic way to achieve this is:

some_object.instance_variable_get("@#{name}")

There is no need to use + or intern; Ruby will handle this just fine. However, if you find yourself reaching into another object and pulling out its ivar, there's a reasonably good chance that you have broken encapsulation.

If you explicitly want to access an ivar, the right thing to do is to make it an accessor. Consider the following:

class Computer
def new(cpus)
@cpus = cpus
end
end

In this case, if you did Computer.new, you would be forced to use instance_variable_get to get at @cpus. But if you're doing this, you probably mean for @cpus to be public. What you should do is:

class Computer
attr_reader :cpus
end

Now you can do Computer.new(4).cpus.

Note that you can reopen any existing class and make a private ivar into a reader. Since an accessor is just a method, you can do Computer.new(4).send(var_that_evaluates_to_cpus)

Ruby: Setting a global variable by name

If you can use an instance variable instead, there is Object#instance_variable_set.

  def baz(symbol)
instance_variable_set("@#{symbol}_bar", 42)
end

Note that it only accepts variable names that can be accepted as an instance variable (starting with @). If you put anything else in the first argument, it will return an error. For the global variable counterpart to it, there is a discussion here: Forum: Ruby

Either way, you also have the problem of accessing the variable. How are you going to do that?

How to get the name of an object in Ruby?

As usual with Ruby, there's a gem for that:
http://thinkrelevance.com/blog/2009/09/23/quick-and-easy-logging-with-logbuddy.html

It will output to stdout.

Thanks to Andrew Grimm (above) and Leventix.

How to call methods dynamically based on their name?

What you want to do is called dynamic dispatch. It’s very easy in Ruby, just use public_send:

method_name = 'foobar'
obj.public_send(method_name) if obj.respond_to? method_name

If the method is private/protected, use send instead, but prefer public_send.

This is a potential security risk if the value of method_name comes from the user. To prevent vulnerabilities, you should validate which methods can be actually called. For example:

if obj.respond_to?(method_name) && %w[foo bar].include?(method_name)
obj.send(method_name)
end

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"

Ruby dynamic variable name

You can do it with instance variables like

i = 0
file.lines do |l|
l.split do |p|
if p[1] == "InitGame"
instance_variable_set("@Game_#{i += 1}", Hash.new)
end
end
end

but you should use an array as viraptor says. Since you seem to have just a new hash as the value, it can be simply

i = 0
file.lines do |l|
l.split do |p|
if p[1] == "InitGame"
i += 1
end
end
end
Games = Array.new(i){{}}
Games[0] # => {}
Games[1] # => {}
...


Related Topics



Leave a reply



Submit