Get Instance Variable Name from Itself in Ruby

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)

How to access instance variable from its string name?

Use instance_variable_set and instance_variable_get. Keep in mind the string needs to have the leading @:

@foo = "bar"
# => "bar"
instance_variable_get("@foo")
# => "bar"
instance_variable_set("@foo", "baz")
# => "baz"
@foo
# => "baz"

Ruby - How to find class name given an instance variable?

You can't, you can get the class name of an instance variable, but the "instance variable of an instance" has its own class (it's still an object).

So student.name.class will return String, student.class will return Student.

If you want such a binding (student name => student class) you have to write your own system to keep track of it. But in any case, your system can't block anyone from writing "John Doe" anywhere and claim its an instance variable for a Student object.

No programming language that currently I'm aware of provides a feature as the one you requested.

Perhaps you want something like student.name = StudentName.new("John Doe")? In this case you can definitely keep track of it, but it's up to you create it and make it works.

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...

Instance variable access in Ruby

In your current code, you're defining an instance variable on the Test class, rather than on instances of Test. That is, you could access it with a class method:

class Test
@state = 4
def self.state
@state
end
end

# Test.state
# => 4

But that's not what you want here; you don't want that value to be present on your class, you want it to be present for each instance of your class. To initialize instance variables on instances of classes, you should provide a constructor:

class Test
attr_accessor :state

def initialize
@state = 4
end

def check_state
puts "state is #{@state}"
end
end

# Test.new.state
# => 4

Getting local variable names defined inside a method from outside the method

You can (re)-parse the method and inspect the S-EXPR tree. See below for a proof of concept. You can get hold of the file where the method is defined using Method#source_location and then read that file. There is surely room for improvement to my code but should get you started. It is a fully functional piece of code and only requires the ruby parser gem (https://github.com/whitequark/parser).

require 'parser/current'
node = Parser::CurrentRuby.parse(DATA.read) # DATA is everything that comes after __END__

def find_definition(node, name)
return node if definition_node?(node, name)
if node.respond_to?(:children)
node.children.find do |child|
find_definition(child, name)
end
end
end

def definition_node?(node, name)
return false if !node.respond_to?(:type)
node.type == :def && node.children.first == name
end

def collect_lvasgn(node)
result = []
return result if !node.respond_to?(:children)

node.children.each do |child|
if child.respond_to?(:type) && child.type == :lvasgn
result << child.children.first
else
result += collect_lvasgn(child)
end
end

result
end

definition = find_definition(node, :foo)
puts collect_lvasgn(definition)

__END__

def foo
var = 100
arr = [1,2]
if something
this = 3 * var
end
end

def bar
var = 200
arr = [3, 4]
end

Do you mind telling us WHY you want to find the variables?



Related Topics



Leave a reply



Submit