Search code examples
rubyyamlsettingsdefined

Odd behavior with Ruby defined?


I am a newbie at Ruby and have a question with the defined? keyword.

Here's a snippet of code that I've written to load a yaml file to initialize settings in my Ruby script:

# Read settings file
require 'YAML'
settingsFile = File.join(File.dirname(__FILE__), "settings.yml").tr('\\', '/')
Settings = YAML.load_file(settingsFile) unless defined? Settings
puts Settings

The yaml file looks like this:

Hello: World

This outputs correctly with:

{"Hello"=>"world"}

Now if I use a variable instead of a constant to store the settings, such as the following:

# Read settings file
require 'YAML'
settingsFile = File.join(File.dirname(__FILE__), "settings.yml").tr('\\', '/')
settings = YAML.load_file(settingsFile) unless defined? settings
puts settings

settings returns empty.

What gives? Why would using a constant make this work?


Solution

  • This is a quirk in the way Ruby handles trailing if/unless conditions and how variables come into existence and get "defined".

    In the first case the constant is not "defined" until it's assigned a value. The only way to create a constant is to say:

    CONSTANT = :value
    

    Variables behave differently and some would argue a lot more strangely. They come into existence if they're used anywhere in a scope, even in blocks of code that get skipped by logical conditions.

    In the case of your line of the form:

    variable = :value unless defined?(variable)
    

    The variable gets "defined" since it exists on the very line that's being executed, it's going to be conditionally assigned to. For that to happen it must be a local variable.

    If you rework it like this:

    unless defined?(variable)
      variable = :value
    end
    

    Then the behaviour goes away, the assignment proceeds because the variable was not defined prior to that line.

    What's strange is this:

    if defined?(variable)
      variable = :value
    end
    

    Now obviously it's not defined, it doesn't get assigned, but then this happens:

    defined?(variable)
    # => "local-variable"
    

    Now it's defined anyway because Ruby's certain it's a variable. It doesn't have a value yet, it's nil, but it's "defined" as far as Ruby's concerned.

    It gets even stranger:

    defined?(variable)
    # => false
    if (false)
      variable = :value
    end
    defined?(variable)
    # => "local-variable"
    

    Where that block of code didn't even run, it can't even run, and yet, behold, variable is now defined. From that assignment line on forward that variable exists as far as Ruby is concerned.

    Since you're doing an attempted assignment and defined? on the same line the variable exists and your assignment won't happen. It's like a tiny paradox.