22

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?

Gabe
  • 84,912
  • 12
  • 139
  • 238
qichunren
  • 4,405
  • 7
  • 37
  • 48

4 Answers4

22

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/

horseyguy
  • 29,455
  • 20
  • 103
  • 145
  • Thanks for this. I get how main is working now. Cool that `MyObject.new.__send__(:test)` works! (Though maybe not useful). Re:`Test1Fixed` I thought I did go onto explain that the `@ff` var is a member of the class, hence why it is nil, `Test2` was supposed to explain this by its output ("`will/will NOT output 'instance'`" depending on if you call `test` or `class_test`) but perhaps I wasn't particularly clear. Also if you wrote/started pry, it's pretty cool bro! – Soup May 28 '12 at 10:29
  • @soup updated the answer to reflect that you did properly explain `@ff`, sorry :) Thx re pry, glad you like it :) – horseyguy May 28 '12 at 18:40
16

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.

Soup
  • 1,699
  • 15
  • 30
  • No, when you define a method at top-level (main) you're actually defining a private instance method on the `Object` class itself. The only way to define a method on just `main` is to define a method on `main`'s singleton class, a la: `def self.hello; end` – horseyguy May 28 '12 at 08:18
  • Actually, re-reading your answer, most of it is completely wrong hehe. – horseyguy May 28 '12 at 09:54
  • Could you edit it or point me to where I can learn more. I haven't studied particular topic (main etc, I _have_ studied ruby scopes/objects, though maybe not to the fullest degree), only put together what I knew/thought I knew about ruby objects/scope. – Soup May 28 '12 at 10:02
2

In ruby, a name without a sigil always has "local" scope. Ruby uses lexical scoping, so inside a method, the name always refers to that method only.

The other scopes are global, instance and class. Here is a good document about them: http://en.wikibooks.org/wiki/Ruby_Programming/Syntax/Operators#Scope

There are a few ways to solve your problem; my favourite would be to pass the thing being printed as an argument:

ff = "ff"
def test(ff)
  puts ff
end

More likely, you'd be encapsulating whatever you're doing in a class and using an instance variable.

You could also just use a global variable ($ff), but that way madness lies.

Tim Peters
  • 4,034
  • 2
  • 21
  • 27
  • If you just want to share a variable in current file, it is better to use `@a` than `$a`, as it could prevent namespace pollution in some extend. – shouya May 28 '12 at 07:43
0

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

Because it's a local variable. Local variables are local to the scope they are defined in. That's why they are called "local variables", after all.

In this case, you have defined a local variable in the script scope, and thus it is visible in the script scope, and nowhere else.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • in many languages i'd think methods are within the scope of the class, so if a variable is local to a class then it's accessible to all methods within that class.. obviously not in the case of ruby though. – barlop May 22 '18 at 04:07
  • also, you write of script scope, but googling ruby "script scope" isn't coming up with anything. It sounds like a good term though. – barlop May 22 '18 at 04:14