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
How to Install Sqlite3 For Ruby on Windows
Difference Between Collection Route and Member Route in Ruby on Rails
Difference Between Include and Extend in Ruby
What Is the Colon Operator in Ruby
Ruby: How to Post a File Via Http as Multipart/Form-Data
How to Fix "Your Ruby Version Is 1.9.3, But Your Gemfile Specified 2.0.0"
Do..End VS Curly Braces For Blocks in Ruby
Best Way to Require All Files from a Directory in Ruby
Ruby - Parameters by Reference or by Value
Rails 4: List of Available Datatypes
Can't Install Gems on Os X "El Capitan"
Ruby 2.0.0P0 Irb Warning: "Dl Is Deprecated, Please Use Fiddle"
How to Get the Name of the Calling Method