4

In Ruby, I am aware that I can do things like this:

if false
  var = "Hello"
end

puts var

The application doesn't crash, and var is simply set to nil. I've read that this happens due to the way the Ruby parser works.

Why does the same not work for constants?

if false
  MY_CONST = "Hello"
end

puts MY_CONST
=> uninitialized constant MY_CONST (NameError)
ardavis
  • 9,842
  • 12
  • 58
  • 112
  • Good question. I'm gonna guess at this. There is likey some lower level metal reason for this. Ruby will also warn you if you try to reinitialize a constant. Allocating some kind of memory before the constant has been defined would likely cause issues it would not with local variables. Also maybe see [this answer](https://stackoverflow.com/questions/1977780/what-does-ruby-constant-mean) for further insight. – lacostenycoder Jun 10 '20 at 11:44
  • 1
    Can you be more precise what kind of answer you are looking for other than "Because the language specification says so?" If you are asking *why* the language specification says so, that cannot be answered on [so], you'd have to ask Yukihiro Matsumoto. – Jörg W Mittag Jun 10 '20 at 12:02
  • 1
    The [docs](https://ruby-doc.org/core-2.7.1/doc/syntax/assignment_rdoc.html#label-Local+Variables+and+Methods) only say that local variables are _"created when the parser encounters the assignment"_ – it doesn't say anything about constants. – Stefan Jun 10 '20 at 12:05
  • @JörgWMittag Thanks for the question. I feel there are many questions that have been answered that discuss why something is the way it is. And you are probably right, I may need to elevate the question. It seemed like the best approach would be to start here. – ardavis Jun 10 '20 at 12:09
  • Not only constants behave in that way. You would also get a `NameError` exception if you do (for instance) `ruby -e 'A=nil; p @@r'`. In this case, the error text is _uninitialized class variable_. – user1934428 Jun 10 '20 at 12:34
  • @JörgWMittag Do you know the best way to ask these kinds of questions? Should this simply be an email to Matz or does he have another method of communication you're aware of that he prefers? – ardavis Jun 10 '20 at 12:53

3 Answers3

2

TL;DR

Local variables are defined when encountered by the parser, while constants are not. However, both must be defined when evaluated by the interpreter to avoid NameError.

Analysis

Local Variables are Auto-Vivified by the Parser

Your original code doesn't actually assign a value to either the local variable or the constant. In both cases, if false is never truthy, so the assigment statements are never executed. Undefined variables and constants are handled differently by the parser, though.

Scoping issues aside, local variables are created when the parser encounters the assignment, not just when the assignment occurs. So, even though:

if false
  var = "Hello"
end

never executes the assignment, it still initializes the local variable to nil.

Constants, on the other hand, are treated differently. An unknown constant (really, anything that starts with an uppercase letter) that isn't available within the current namespace will raise a NameError.

In a fresh irb session, both of these will raise NameError, but with slightly different exception messages:

puts var
#=> NameError (undefined local variable or method `var' for main:Object)

puts MY_CONST
#=> NameError (uninitialized constant MY_CONST)

However, if you change your branch logic so that an expression with an undefined variable is evaluated by the interpreter, you'll also get NameError:

if baz
  puts true
end

#=> NameError (undefined local variable or method `baz' for main:Object)

Another Way to Examine the Behavior

Fire up a fresh irb session. Then:

irb(main):001:0> defined? var
#=> nil
irb(main):002:0> if false then var = 1 end
#=> nil
irb(main):003:0> defined? var
#=> "local-variable"

You can see that var is both defined and set to nil when encountered by the parser, even though the assignment expression is never evaluated. The constant is not auto-vivified, though:

irb(main):004:0> defined? MY_CONST
#=> nil
irb(main):005:0> if false then MY_CONST = 1 end
#=> nil
irb(main):006:0> defined? MY_CONST
#=> nil
irb(main):007:0> MY_CONST
#=> NameError (uninitialized constant MY_CONST)

Conclusions

While I would guess this behavior has to do with differences between the parser and the interpreter, and perhaps between the namespaces used for variable/method lookup vs. constant lookup, I can't really tell you why the difference is necessary (if it really is), or even if it's the same across all Ruby implementations. That's a question for the various Ruby engine developers, including the Ruby Core Team.

Pragmatically, though, you will always get a NameError exception when you attempt to use an undefined variable or constant. The real-world impact (if any) of this difference is therefore minimal. All languages have quirks; this may be one of them, but it's hard to see how this would cause practical problems outside of contrived examples. Your mileage may certainly vary.

Todd A. Jacobs
  • 81,402
  • 15
  • 141
  • 199
1

It's difficult to answer this question from the point of view of the implementation not being Ruby core member, but from the design point of view it is completely logical.

if false
  var = 'hello'
end

In this case you may want to do something with your variable further, i.e. try to reassign it with ||= or make some decisions based on whether or not the variable is nil. And the language design will allow that because this is a variable (as in not a constant).

Constants, on the other hand, are class members and defined on the class scope:

initial_constants = self.class.constants

# Does the same as the self.class::MY_CONST = 'hello'
MY_CONST = 'hello'

> self.class::MY_CONST
=> "hello"

> self.class.constants - initial_constants
=> [:MY_CONST]

and usually you don't have evaluations on the class level. You either have a constant or you don't.

To confirm that, outside of the simple script case which is presented in your question, if you'll try to define a constant this way on the instance level:

def hello
  if false
    MY_CONST = 'hello'
  end
end

you'll get the exception:

SyntaxError: dynamic constant assignment

And most of the real world Ruby programs are object-oriented.

Finally, one of the main questions that a language designer probably had to answer to himself are:

  • Why in the world will somebody need dynamically defined constants?
  • (And as a consequence) Why in the world will somebody need a stored constant with a nil value?
jibiel
  • 8,175
  • 7
  • 51
  • 74
  • Thank you so much for your detailed response! This question came about due to trying to clean-up some code that looks irregular. We had some constants that were essentially `CONST = if condition; a; else; nil; end`. We assumed we could flip it to `CONST if a if condition` and were quite wrong! I'm dealing with legacy code and chances are constants are being used improperly. – ardavis Jun 10 '20 at 13:18
0

You never assign the constant! Compare your code to this more extended example:

if f
   A=5
   B=8
else
   A=9
   C=7
end

and remember, that everything in Ruby is executable code; there are no declarations. Now if f is truthy, you create the constants A and B, and if f is falsy, you get the constants A and C.

In your code, you have a if false, so the constant is not created.

BTW, the word constant is IMO a misnomer, because you can change constants (either with or without getting a warning, depending on how you do it).

user1934428
  • 19,864
  • 7
  • 42
  • 87
  • The `if false` is an example. Essentially we were going through our code doing some streamlining of short if-statements. Changing many things to `a = b if condition`. We had a case that was setting the constant to a value or nil based on a condition. During the attempt to streamline (assuming it would be set to nil) we found the issue and realized we couldn't condense it in that format (I know there are other ways, ternary, etc.) – ardavis Jun 10 '20 at 12:30
  • If you want to create a framework to process possibly uninitialized constants (for instance let them spring into existence initialized with `nil`, if they are not defined), you can use the methods `const_defined?` and `const_get` in `Module`. – user1934428 Jun 10 '20 at 12:40