DEV Community

Akshay Khot
Akshay Khot

Posted on • Edited on • Originally published at akshaykhot.com

How to Check if a Variable is Defined in Ruby

This post was originally published on my blog: How to Check if a Variable is Defined in Ruby


Ruby provides a handy defined?(expression) keyword that tests if the expression refers to anything recognizable. The expression can be an object, a variable that's initialized, method name, etc. If Ruby can't resolve the expression, it returns nil. Otherwise, it returns a string describing the expression.

Here are some examples of using defined? with different types of expressions. Note that a variable set to nil is still initialized and recognized by ruby.

RSpec.describe 'Defined' do
  it 'tests if the local variable is defined' do
    name = 'Akshay'

    expect(defined? name).to eq('local-variable')
    expect(defined? b).to eq(nil)
    expect(defined? nil).to eq('nil')
    expect(defined? String).to eq('constant')
    expect(defined? 1).to eq('expression')
  end

  it 'ensures that a variable set to nil is still recognized' do
    name = nil
    expect(defined? name).to eq('local-variable')
  end
end
Enter fullscreen mode Exit fullscreen mode

Using with conditional assignment

Sometimes, you want to lazily evaluate some code, only once. That is, do nothing if a variable exists but initialize it if it doesn't. The idiomatic ruby approach is to use the ||= operator.

def result
  @result ||= calculate_result
end

def calculate_result
  puts '>>> heavy calculation here.. should happen only once'
  100
end

it 'lazy-evaluates the calculate_result operation once' do
  expect(result).to eq(100)
  expect(result).to eq(100)
end

# Output

>>> heavy calculation here.. should happen only once
Enter fullscreen mode Exit fullscreen mode

Just make sure you don't use it with an operation that can return nil or boolean value false as the result. Otherwise, it will invoke the calculate_result every time, eliminating the benefit of ||= operator.

For example, if you change the calculate_result method above to return false (or nil) instead of 100, ruby will call calculate_result each time result is called.

def calculate_result
  puts '>>> heavy calculation here.. should happen only once'
  false # or nil
end

# Output

>>> heavy calculation here.. should happen only once
>>> heavy calculation here.. should happen only once
Enter fullscreen mode Exit fullscreen mode

The defined? method comes in handy in such cases. Change the result method so it first checks if the @result variable is defined.

def result
  return @result if defined? @result
  @result = calculate_result
end
Enter fullscreen mode Exit fullscreen mode

Don't use defined? to check hash keys

A common mistake is to use defined? to verify if a hash key is defined. For example,

def check_hash_key
  hash = {}
  defined?(hash['key']) ? 'unexpected!' : 'not defined'
end

it 'does not return false for non-existing hash key' do
  expect(check_hash_key).to eq('unexpected!')
end
Enter fullscreen mode Exit fullscreen mode

This is because it returns the string method, which ruby evaluates to true in a boolean expression.

it 'returns method for non-existing hash key' do
    data = {}
    expect(defined? data['key']).to eq('method')
end
Enter fullscreen mode Exit fullscreen mode

The idiomatic ruby solution to check if a key exists in a hash is to use any of the following methods: has_key?, key?, include?, or member?

data = {}
expect(data.key?('key')).to eq(false)
Enter fullscreen mode Exit fullscreen mode

Use parenthesis when using defined?

You don't always need to use them, but it's highly recommended due to the low precedence of defined? keyword. For example, if you want to check if a variable exists and it's greater than zero, you might write something like this.

it 'has low precedence' do
    result = 10
    expect(defined? result && result > 0).to eq(true)
end

# Fails!
# expected: true
# got: "expression"
Enter fullscreen mode Exit fullscreen mode

Adding parentheses results in a better check, and it's also very clear. The following test passes with flying colors.

it 'has low precedence' do
    result = 10
    expect(defined? result && result > 0).to eq('expression')
    expect(defined?(result) && result > 0).to eq(true)
end
Enter fullscreen mode Exit fullscreen mode

Top comments (0)