3

I was playing with the assignment operation within if blocks, and discovered the below result, which surprised me:

C:\>irb --simple-prompt
if false
x = 10
end
#=> nil
p x
nil
x.object_id
#=> 4
#=> nil
p y
NameError: undefined local variable or method `y' for main:Object
        from (irb):5
        from C:/Ruby193/bin/irb:12:in `<main>'

In the above code you can see that the x local variable has been created even though it was only assigned to in the falsy if block. I tried to to see the content of x with p x which forced me to believe that assignment was not done, but the x variable exists. x.object_id also proved that is the case.

Now my question is how that x local variable was created even though the if block entry point is closed forever intentionally?

I expected the output of p x to be similar to the output from p y. But instead I got a surprising answer from p x.

Could someone explain to me how this concept works?

EDIT

No, here is another test. This is not the case with only local variables. The same happened with instance and class variables also. See the below:

class Foo
  def show
    @X = 10 if false
    p @X,"hi",@X.object_id
  end
end
#=> nil
Foo.new.show
nil
"hi"
4
#=> [nil, "hi", 4]

class Foo
  def self.show
    @@X = 10 if false
    p @@X,"hi",@@X.object_id
  end
end
#=> nil
Foo.show
nil
"hi"
4
#=> [nil, "hi", 4]

Successful case :

class Foo
  def self.show
    @@X = 10 if true
    p @@X,"hi",@@X.object_id
  end
end
#=> nil
Foo.show
10
"hi"
4
#=> [10, "hi", 4]
Ajedi32
  • 45,670
  • 22
  • 127
  • 172
Arup Rakshit
  • 116,827
  • 30
  • 260
  • 317

3 Answers3

8

In Ruby, local variables are defined by the parser when it first encounters an assignment, and are then in scope from that point on.

Here's a little demonstration:

foo # NameError: undefined local variable or method `foo' for main:Object

if false
  foo = 42
end

foo # => nil

As you can see, the local variable does exist on line 7 even though the assignment on line 4 was never executed. It was, however, parsed and that's why the local variable foo exists. But because the assignment was never executed, the variable is uninitialized and thus evaluates to nil and not 42.

In Ruby, most uninitialized or even non-existing variables evaluate to nil. This is true for local variables, instance variables and global variables:

defined? foo       #=> nil
local_variables    #=> []
if false
  foo = 42
end
defined? foo       #=> 'local-variable'
local_variables    #=> [:foo]
foo                #=> nil
foo.nil?           #=> true

defined? @bar      #=> nil
instance_variables #=> []
@bar               #=> nil
@bar.nil?          #=> true
# warning: instance variable @bar not initialized

defined? $baz      #=> nil
$baz               #=> nil
# warning: global variable `$baz' not initialized
$baz.nil?          #=> true
# warning: global variable `$baz' not initialized

It is, however, not true for class hierarchy variables and constants:

defined? @@wah     #=> nil
@@wah
# NameError: uninitialized class variable @@wah in Object

defined? QUUX      #=> nil
QUUX
# NameError: uninitialized constant Object::QUUX

This is a red herring:

defined? fnord     #=> nil
local_variables    #=> []
fnord
# NameError: undefined local variable or method `fnord' for main:Object

The reason why you get an error here is not that unitialized local variables don't evaluate to nil, it is that fnord is ambiguous: it could be either an argument-less message send to the default receiver (i.e. equivalent to self.fnord()) or an access to the local variable fnord.

In order to disambiguate that, you need to add a receiver or an argument list (even if empty) to tell Ruby that it is a message send:

self.fnord
# NoMethodError: undefined method `fnord' for main:Object
fnord()
# NoMethodError: undefined method `fnord' for main:Object

or make sure that the parser (not the evaluator) parses (not executes) an assignment before the usage, to tell Ruby that it is a local variable:

if false
  fnord = 42
end
fnord              #=> nil

And, of course, nil is an object (it is the only instance of class NilClass) and thus has an object_id method.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • when I did the below `>> local_variables #=> [:name, :_]`. What is `:_` ? – Arup Rakshit Mar 03 '13 at 14:58
  • Well, it's another local variable. I can't tell you where it comes from, you would have to show your entire code for that. However, if you tried that in IRb, then the answer is obvious: it's the local variable defined by IRb that always holds the value of the last expression that was evaluated. – Jörg W Mittag Mar 03 '13 at 16:14
  • Yes. I typed it in `IRB`. here is the code `>> local_variables #=> [:_] >> if false >> name = "joy" >> end #=> nil >> local_variables #=> [:name, :_]` – Arup Rakshit Mar 03 '13 at 16:20
  • Okay, so I tried it now - `local_variables #=> [:_] if false name = "joy" end #=> nil local_variables #=> [:name, :_] a = 2 + 3 #=> 5 local_variables #=> [:a, :name, :_] p :_ :_ #=> :_` But couldn't see any values in `:_` :( – Arup Rakshit Mar 03 '13 at 16:23
  • The name of the variable is `_`. `:_` is a symbol literal denoting the name of the variable. – Jörg W Mittag Mar 03 '13 at 16:26
  • yes, you are right - here is my try `>> 2 + 3 => 5 >> p "#{_}" "5" => "5" ` – Arup Rakshit Mar 03 '13 at 16:28
  • Worth mentioning that this behavior is not just a Rubyism; it's an artifact of allowing variables to be used in the scope of their definition even before the definition itself (called "hoisting"). Javascript does the same thing: an undefined var raises a ReferenceError, but if you do `if (false) { var fnord=42; }`, a reference to `fnord` just returns `undefined` with no exception. – Mark Reed Nov 26 '13 at 18:38
3

Ruby always parses all of your code. It doesn't look at false as a sign to not parse what's inside, it evaluates it and sees that the code inside shouldn't be executed

enthrops
  • 729
  • 5
  • 14
1

Ruby has local variable "hoisting". If you have an assignment to a local variable anywhere within a method, then that variable exists everywhere within the method, even before the assignment, and even if the assignment is never actually executed. Before the variable is assigned, it has a value of nil.

Edit:

The above is not quite correct. Ruby does have a form of variable hoisting in that it will define a local variable when a local variable assignment is present, but not executed. The variable will not be found to be defined at points in the method above where the assignment occurs, however.

Steve Jorgensen
  • 11,725
  • 1
  • 33
  • 43
  • But how it enters into the `If` block whereas the entry point is locked with `false`. Who break that lock? – Arup Rakshit Mar 03 '13 at 07:59
  • It is not the case with always `local` variables. – Arup Rakshit Mar 03 '13 at 08:32
  • -1. This is wrong. Local variables only exist *after* they have been defined. You're probably thinking of JavaScript. See http://stackoverflow.com/a/15111934/2988 for a demonstration. – Jörg W Mittag Mar 03 '13 at 13:33
  • 1
    I was thinking of Ruby, not Javascript, and Ruby does have a form of variable hoisting. I see that you are correct, however, that the variable is only found to be defined later in the method than where the first variable assignment occurs. The variable is still hoisted, however, in that it is defined even if the assignment is NOT executed. – Steve Jorgensen Mar 03 '13 at 18:37
  • To clarify Jörg's comment above - "after" doesn't mean execution-wise, but appearance in the source code when read left-to-right, top-to-bottom. – Kelvin Jul 03 '14 at 18:26