Ruby: Retrieve local variable from class - ruby

I was reading up on metaprogramming and came across this exercise:
http://ruby-metaprogramming.rubylearning.com/html/Exercise_1.html
The question is:
Given this class definition:
class A
def initialize
#a = 11
##a = 22
a = 33
end
#a = 1
##a = 2
a = 3
end
Retrieve all the variables from outside the class, with output like so:
1
2
3
11
22
33
Now, it's pretty straightforward to get the instance and class variables dynamically, even the local variable inside the constructor (it's the return value of the initialize method). But I'm stumped as to how to get the local variables a=3.
As far as I know, this is not possible because the local variables cease to exist after the class definition is first read.
The only roundabout way that I have made this work is to set a variable to the "return" value (for lack of a better term) of when the class is declared, like so:
val = class A
a = 3
end
puts val # => 3
Is this the only way?

The question seems to boil down to this: given the following:
class A
attr_reader :instance_var
def initialize
#instance_var = (#instance_var ||= 0) + 1
instance_local_var = 33
puts "instance_local_variables = #{ local_variables }"
instance_local_var = 33
end
class_local_var = 3
puts "class_local_variables = #{ local_variables }"
class_local_var = 3
end
# class_local_variables = [:class_local_var]
#=> 3
can one determine the values of instance_local_var and class_local_var?
Determine the value of class_local_var
The answer to this question is clearly "no" because class_local_var no longer exists (has been marked for garbage collection) after end is executed1:
A.send(:local_variables)
#=> []
Determine the value of instance_local_var
a = A.new
# instance_local_variables = [:instance_local_var]
#=> #<A:0x007ff3ea8dbb80 #instance_var=1>
Note that #instance_var #=> 1.
A.new does not return the value of instance_local_var, but because that variable is assigned a value in the last line of initialize, that value can be obtained by executing initialize once again.2
instance_local_var = a.send(:initialize)
#=> 33
There is a problem, however:
a.instance_var
#=> 2
Executing initialize a second time has caused an unwanted side effect. My definition of initialize is artificial, but it highlights the fact that many undesirable side effects could occur by executing initialize a second time.
Now let's obtain a new instance.
b = A.new
# instance_local_variables = [:instance_local_var]
#=> #<A:0x007fee0996e7c8 #instance_var=1>
Again, #instance_var=1. One possible workaround to the side-effects of calling initialize twice for a given instance is to subclass A and use super.
class B < A
attr_reader :b
def initialize
#b = super
end
end
B.new.b
#=> 33
a.instance_var
#=> 1
There is no guarantee that undesirable side-effects can be avoided with this approach (e.g., initialize for any instance may perform a database operation that should occur only once), but it appears to leave the initial instance a unaffected. This is of course all hypothetical.
1. send must be used because A.private_methods.include?(:local_variables) #=> true
2. A.new.send(:initialize) is required because iniitialize is private.

Your question is unclear. In the title, you write "local variables", but in the example, you mention only instance- and class-variables.
As for the instance variables, you can use Object#instance_variables to get a list of known instance variables at this point. Note however that instance variables are created dynamically, not at the time of class declaration. For example, given the class
class AA;
def initialize; #x=1; end;
def f; #y=1; end;
end
the expression
AA.new.instance_variables
would return [:#x] - the :#y is missing, because it doesn't exist yet.
You don't have a way to automatically (i.e. without modifying the class) retrieve local variables. As mudasobwa explained in his answer, you would have to explictly pass back a binding.

One might get an access to all the local variables of the class by returning it’s binding from the class definition (see Binding#local_variables for details):
a = class A
v1 = 3.14
v2 = 42
binding
end
a.local_variable_get(:v1)
#⇒ 3.14
a.local_variable_get(:v2)
#⇒ 42
But the main question is why would you want to do that? The local variable is meant to remain local. This is how it is supposed to behave. Also, one is unable to modify the local variable in the original binding (what is returned is a read-only copy of an original binding.)

Related

Why is `instance_variable_set` necessary in Ruby?

What's the point of instance_variable_set? Aren't these two lines the same?
instance_variable_set(#name, value)
#name = value"
In the case of a "simple" variable assignment for an instance variable like:
#foo = "foo"
You couldn't do
"##{foo}" = "bar" # syntax error, unexpected '=', expecting end-of-input
But you could do something similar with instance_variable_set:
instance_variable_set("##{foo}", "bar")
p #foo # "bar"
As per your question Aren't these two lines the same?, for that example they're similar, but isn't the use people tend to give to instance_variable_set.
I wonder why there is no mention of another obvious difference: they have different scopes.
While #name = value is accessible only from within the scope where the instance variable is defined (read: from inside the instance,) instance_variable_set is available from everywhere to set instance variables from outside:
class C
attr_reader :name
def initialize(name)
#name = name
end
end
C.new("foo").tap do |c|
c.instance_variable_set(:#name, 42)
c.name
end
#⇒ 42
I'm pretty new to Ruby and have this question when I'm reading some tutorial. I'm curious what's the point of instance_variable_set?
The point of Object#instance_variable_set is to dynamically reflectively set an instance variable whose name may not be known at design time, only at run time.
Aren't these two lines the same?
instance_variable_set(#name, value)
#name = value
No, these lines are completely different, and they perfectly illustrate what I wrote above:
The first line sets the instance variable whose name is stored inside #name to value.
The second line sets the instance variable #name to value.
From the fine manual:
instance_variable_set(symbol, obj) → obj
instance_variable_set(string, obj) → obj
Sets the instance variable named by symbol to the given object, thereby frustrating the efforts of the class's author to attempt to provide proper encapsulation. The variable does not have to exist prior to this call. If the instance variable name is passed as a string, that string is converted to a symbol.
So the first argument isn't #name, it is :#name (i.e. a Symbol) or '#name' (a String).
The result is that instance_variable_set, as noted in the documentation, can be used to set an instance variable when you know its name even if you don't know the name until your code is running.
Here is an example of how the methods Object#instance_variable_set and Object#instance_variable_get could be used to increment the values of all instance variables by one.
class Klass
attr_accessor :a, :b, :cat
def initialize
#a, #b, #c, #d = 1, 2, 3, 4
end
end
k = Klass.new
#=> #<Klass:0x0000000001d70978 #a=1, #b=2, #c=3, #cat=4>
k.instance_variables.each { |v| k.instance_variable_set(v, k.instance_variable_get(v)+1) }
#=> [:#a, :#b, :#c, :#cat]
k #=> #<Klass:0x0000000001d70978 #a=2, #b=3, #c=4, #cat=5>
See also Object#instance_variables.
Compared to having four separate assignment statements, fewer lines of code are needed, but there are two other, more important advantages:
there is less chance of introducing a bug (k.cut += 1); and
adding, removing or renaming instance variables does not require the value-incrementing code to be changed.
A variant of this is to substitute a dynamically-constructed array of instance variable names (e.g., [:#a, :#b]) for instance_variables above.
These may seem like unusual examples, but they are representative of a large class of operations involving instance variables in which this kind of batch processing can be used to advantage.

Local variables in class definitions / scope

I was under the impression that class definitions in Ruby can be reopened:
class C
def x
puts 'x'
end
end
class C
def y
puts 'y'
end
end
This works as expected and y is added to the original class definition.
I'm confused as to why the following code doesn't work as expected:
class D
x = 12
end
class D
puts x
end
This will result in a NameError exception. Why is there a new local scope started when a class is reopened? This seems a little bit counterintuitive. Is there any way to continue the previous local scope when a class is extended?
Local variables are not associated with an object, they are associated with a scope.
Local variables are lexically scoped. What you are trying to do is no more valid than:
def foo
x = :hack if false # Ensure that x is a local variable
p x if $set # Won't be called the first time through
$set = x = 42 # Set the local variable and a global flag
p :done
end
foo #=> :done
foo #=> nil (x does not have the value from before)
#=> done
In the above it's the same method, on the same object, that's being executed both times. The self is unchanged. However, the local variables are cleared out between invocations.
Re-opening the class is like invoking a method again: you are in the same self scope, but you are starting a new local context. When you close the class D block with end, your local variables are discarded (unless they were closed over).
In ruby, local variables accessible only in scope that they are defined. However, the class keywords cause an entirely new scope.
class D
# One scope
x = 12
end
class D
# Another scope
puts x
end
So, you can't get access to local variable that defined in first class section, because when you leave first scope, local variable inside it is destroyed and memory is freed by garbage collection.
This is also true for the def, for example.
The easy solution would be to make x a class instance variable. Of course, this doesn't answer your scope question. I believe the reason x is not in the scope (see Detecting the Scope of a Ruby Variable) of the re-opened class is because its scope was never that of class D in the first place. The scope of x ended when class D closed because x is neither an instance variable nor a class variable.
I hope that helps.
A part of the reason of why this behavior makes sense lies with metaprogramming capabilities; you could be using some temporary variables to store data that you could use to name a new constant, or to reference a method name:
class A
names, values = get_some_names_and_values()
names.each_with_index do |name, idx|
const_set name, value[idx]
end
end
Or maybe, you need to get the eigenclass...
class B
eigenclass = class << self; self; end
eigenclass.class_eval do
...
end
end
It make sense to have a new scope every time you reopen a class, because in Ruby you often write code inside a class definiton that is actual code to be executed and opening a class is just a way to get the right value for self. You don't want the content of the class to be polluted by these variables, otherwhise you'd use a class instance variable:
class C
#answer = 42
end
class C
p #answer
end

Hook to be called when a variable changes

Was there a hook in ruby that is called every time the value of a certain variable changes?
If you write a C extension for Ruby, you can actually make a global variable that triggers a setter hook whenever someone sets it.
But you probably don't want to do that because you'd have to write some C and it could be a pain to manage that.
A better strategy would be to make it so that the variable is read and set through appropriate methods. Then when the setter method is called you can do whatever you want. Here is an example that encapsulates a variable inside an object:
class Foo
def bar=(v)
#bar = v
# do some stuff
end
def bar
#bar
end
end
Similarly you could encapsulate the variable in a module or class instead of an object.
Preamble: I this is not a solution (pst already wrote: There is none), but perhaps it can help in special situations.
My first idea was to use freeze to get a solution:
a = "aa"
a.freeze
a << 'b' #can't modify frozen string (RuntimeError)
Now we must redefine freeze and we get a hint, when the variable changes the value:
module FreezeWarning
def freeze
puts "#{self}: I am changed"
end
end
a = "aa"
a.extend(FreezeWarning)
a.freeze
a << 'b' #aa: I am changed
First problem: There is no way to get the variable name.
You may solve this with an additional variable (You can define your own variable identification, it must not be the name)
module FreezeWarning
def change_warning(name)
#varname = name
self.freeze
end
def freeze
puts "<#{#varname}> (#{self}): I am changed"
end
end
a = "aa"
a.extend(FreezeWarning)
a.change_warning('a')
a << 'b' #<a> (aa): I am changed
But the bigger problem: This works only with changes of the value, not with new assignments:
a = 5
a.freeze
a = 4
p a # -> 4
So this is only a very restricted 'solution'.

what is local scope exactly?

To be honest, I still confused about the instance variable and local variable, not sure which should be used.
only one condition I know about local variable that can't be used is:
class MyClass
def initialize
local_var = 1
#instance_var = 1
end
def show_local_var
local_var
end
def show_instance_var
#instance_var
end
end
apparently, MyClass.new.show_instance_var works while MyClass.new_show_local_var not
the other thing about the two kind of variables is that the block seems share the same local scope, so the local variable can be referenced:
local_var = 1
3.times do
puts local_var
end
There are all I know about the distinctions, is there any other available? please let me know
if there is any articles about this, that would be so helpful for me,
A local variable is used "right here right now" and can't be accessed from anywhere else.
class MyClass
def foo
local_var = 2
#I use this to do some sort of calculation
#instance_var = local_var + 34
local_var = 5 * #instance_var
puts local_var
end
# From here, local_var is unaccessible.
end
Once you're out of scope (foo's end is passed) local_var is no more and can't be referred to.
The instance variable is available to the whole class at all times.
class MyClass
def initialize
#instance_var = 0
end
def foo
local_var = 2
#I use this to do some sort of calculation
#instance_var = local_var + 34
end
def some_operation
if #instance_var == 36
#instance_var = 3
else
#instance_var = 1
end
end
end
So when you call m = MyClass.new and later on m.some_operation, it's working with the same #instance_var .
And while we're at it, there are also Class variables (defined ##class_var) that are accessible from any instance of the class.
I don't have an article in particular to provide you, but some googling about ruby variable scope and about each type of variable independently should provide you with all the information you need!
The second example you describe is called a Closure. Paul puts it quite nicely there:
A closure is a block of code which meets three criteria:
It can be passed around as a value and
executed on demand by anyone who has that value, at which time
it can refer to variables from the context in which it was created (i.e. it is closed with respect to variable access, in the mathematical sense of the word "closed").
For a nice, quick introduction about the available scopes in Ruby you may refer to the Ruby Programming wikibook.
There is
Local Scope
Global Scope
Instance Scope
Class Scope
The "Default Scope", as it is sometimes referred to when executing code with no scope modifiers surrounding it, as in
#iv = "Who do I belong to?"
is the scope of the 'main' object.
Local scope is limited to the location in which the variable is declared, i.e. a function, or a for loop, but cannot be accessed from outside that location. However, if you nest other constructs within the function, for loop, etc. then the inner constructs can access the local variable. Instance variables are scoped to the instance of the class.
Article on ruby variable scope
Article on scope in general

metaprogramming access local variables

class Foo
def initialize
bar = 10
end
fiz = 5
end
Is there a possibility to get these local values (outside the class) ?
The local variable in initialize would be lost.
You are able to get the value fiz outside of the class, but only upon defining that class, and recording the return of the definition of the class.
return_of_class_definition = (class A ; fiz = 5 ; end) would assign the value of fiz to the variable.
You can also use binding but of course, this means changing the class, which may not be allowed for the exercise.
class A
bin = 15
$binding = binding
end
p eval 'bin', $binding
No. Once a local variable goes out of scope (for bar that is when the initialize method has run - for fiz when the end of the class definition has been reached), it's gone. No trace left.
While a local variable is still in scope you can see it (well, its name) with local_variables and get and set its value with eval (though that's definitely not recommended for sanity reasons), but once it's out of scope, that's it. No way to get it back.
In ruby we have something we could call scope gates - places when a program written in ruby leaves the previous scope. Those gates are: class, module and method (def keyword). In other words after class, module of def keyword in the code you're immediately entering into a new scope.
In ruby nested visibility doesn't happen and as soon as you create a new scope, the previous binding will be replaced with a new set of bindings.
For example if you define following class:
x = 1
class MyClass
# you can't access to x from here
def foo
# ...from here too
y = 1
local_variables
end
end
local_variables method call will return [:y]. It means that we don't have an access to the x variable. You can workaround this issue using ruby's technique called Flat Scopes. Basically instead defining a class using class keyword you can define it using Class.new and pass a block to this call. Obviously a block can take any local variables from the scope where it was defined since it's a closure!
Our previous example could be rewritten to something like like that:
x = 1
Foo = Class.new do
define_method :foo do
i_can_do_something_with(x)
y = 1
local_variables
end
end
In this case local_variables will return [:x, :y].

Resources