Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't I access a local variable inside a method in Ruby?

Tags:

ruby

I have a Ruby file named test.rb

ff="ff"
def test
  puts ff
end

I execute it, got error:

test.rb:3:in `test': undefined local variable or method `ff' for main:Object (NameError)

What's the reason for this? Is there any documentation to explain it?

like image 692
qichunren Avatar asked May 28 '12 03:05

qichunren


People also ask

How do you use local variables in Ruby?

A local variable is only accessible within the block of its initialization. Local variables are not available outside the method. There is no need to initialize the local variables. Instance Variables: An instance variable name always starts with a @ sign.

Can local variables be declared inside a method?

Local Variables We can put it in a method body. We declared num in the method body of main(). Variables declared inside a method are called local variables. Local variables can only be used within the method body.

Can we access local variable outside the block method?

Well no, it wouldn't. Local variables (declared within a method) only exist for the lifetime of that method call, within the method.

How do you access variables in Ruby?

Here is an example showing the usage of global variable. NOTE − In Ruby, you CAN access value of any variable or constant by putting a hash (#) character just before that variable or constant.


2 Answers

The reason ff is inaccessible inside the test method definition is simply that methods (created with the def keyword) create a new scope. Same with defining classes and modules using the class and module keywords respectively.

The role of main (the top-level object) is almost completely irrelevant to the question of scope in this situation.

Note that, if you DO want your test method to have access to locals defined in the definition context, then use the define_method (or in your case, the define_singleton_method method), see here:

ff = "hi"
define_singleton_method("test") { ff }
test #=> "hi"

Unlike the def keyword, the define_method family of methods do not create new scopes but instead close over the current scope, capturing any local variables.

The reason using @ff worked in the next example given by @soup, is not that main is somehow a "special case" it's just that an ivar defined at top-level is an ivar of main and so is accessible to a method invoked on main.

What, however, is the relationship of the test method to main? It is not a method on just main itself - it is actually a private instance method defined on the Object class. This means that the test method would be available (as a private method) to nearly every object in your ruby program. All methods defined at top-level (main) are actually defined as private instance methods on the Object class.

For more information on the Ruby top-level, see this article: http://banisterfiend.wordpress.com/2010/11/23/what-is-the-ruby-top-level/

like image 184
horseyguy Avatar answered Oct 05 '22 22:10

horseyguy


Ruby scope is both simple and complex.

First you have to remember that everything is an object and everything has a scope.

To answer your question directly, main is an object and such when you write def x... you're defining a method on the object main

Observe in pyr/irb:

# note the error describes 
[1] pry(main)> main 'main:Object'
NameError: undefined local variable or method 'main' for main:Object
from (pry):1:in '<main>'

# self is the current object, which is main
[2] pry(main)> self
=> main 

# note main is an object of type Object
[3] pry(main)> self.class
=> Object 

# main has methods
[4] pry(main)> self.methods
=> [:to_s, :public, etc etc]

So when you write

ff = "ff"
def test
    puts ff
end

what you're really doing is

class main
    ff = "ff"
    def test
        puts ff
    end
end

Which doesn't work because ff is out of scope. To fix this you have to turn ff into an instance variable, so prepend its name with @. The following will work:

@ff = "ff"
def test
    puts @ff
end

test

Note that this seems to be a special case for main than regular classes, see below.

If we have our own test class:

class Test1
    ff = "ff"
    def test
        puts ff
    end
end

Test1.new.test # undefined variable/method 'ff' error

This fails because ff is not defined in the correct scope as expect. It is instead scoped to when the parser is executing our class declaration.

So lets try the fix above:

class Test1Fixed
    @ff = "ff"
    def test
        puts @ff
    end
end

Test1Fixed.new.test # results in blank line

Thats weird.

Lets add this method:

def ff?
    puts "ff is: #{@ff.nil? ? "nil" : "not nill"}"
end

Test1Fixed.new.ff? # => ff is: nil

Why is @ff nil?

The following might make this more clear:

class Test2
    puts "Where am i?"
    @instance = "instance"

    def initialize
        puts "Initalize"
    end

    puts "pants on"

    def test
        puts "This will NOT output 'instance': #{@instance}"
    end

    def self.class_test
        puts "This WILL output 'instance': #{@instance}"
    end
end

puts "Creating new Test2 Object"
t = Test2.new
puts "Calling test on Test2 Object"
t.test

puts "Calling class_test on Test2 Class object" 
Test2.class_test

Running this we get the following output:

$ ruby scope.rb 
Where am i?
pants on
Creating new Test2 Object
Initalize
Calling test on Test2 Object
This will NOT output 'instance': 
Calling class_test on Test2 Class object
This WILL output 'instance': instance

As you can see, the interperter runs over our class declaration in order, just like anywhere else and outputs our puts statements. This gets more interesting when we start calling methods. Writing @instance = ... is the same as writing self.instance = ..., so can you guess where we've defined instance? Yep, on the Test2 class object (not a Test2 object).

This is why when we call test, nothing is outputed, because inside test, self refers to the instanciated Test2 object, not the Test2 class object (which is where we set @instance to be anything!). This is why you setup your instance variables inside initialize, where self will point to the actual object.

You can see when we define a class method via self.class_test and then call that method on the Test2 class object (Test2.class_test), we get the expected output, because in that scope, @instance was defined.

Hope this made some sense.

The http://rubykoans.com/ scope section may help solidify some of this knowledge.

like image 35
Soup Avatar answered Oct 05 '22 22:10

Soup