Ruby provides a handy defined?(expression)
keyword that tests if the expression
refers to anything recognizable. The expression
can be an object, a variable, a 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.
language = 'Ruby'
defined? language # 'local-variable'
defined? @instance_var # 'instance-variable'
defined? @@class_var # 'class variable'
defined? 'Ruby' # 'expression'
defined?(some_random_variable) # nil
defined? nil # 'nil'
defined? String # 'constant'
defined? 1 # 'expression'
site = nil
defined? site # 'local-variable'
Note that a variable set to nil
is still initialized and recognized by ruby as a local-variable
.
Lazy-Evaluation + Caching of Expensive Operations
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 way to accomplish this is to use the ||=
operator.
class Task
def result
@result ||= calculate_result
end
def calculate_result
puts "heavy calculation here... should happen only once"
100
end
end
t = Task.new
puts t.result
puts t.result
# Output
#
# heavy calculation here... should happen only once
# 100
# 100
It works... most of the time. If you have an operation that can return nil
or boolean value false
as result, the cached method will be called each time you call result
, eliminating the benefit of the ||=
operator.
For example, let's change the calculate_result
method above to return false
(or nil
) instead of 100 and see what happens.
class Task
def result
@result ||= calculate_result
end
def calculate_result
puts "heavy calculation here... should happen only once"
false # or nil
end
end
t = Task.new
puts t.result
puts t.result
# Output
#
# heavy calculation here... should happen only once
# false
# heavy calculation here... should happen only once
# false
In such cases, the defined?
method comes in handy. 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
Now, the calculate_result
will be executed only once. For the subsequent calls, the instance variable @result
is defined, so Ruby won't execute the second line of code above.
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,
hash = {}
if defined?(hash['random_key'])
puts 'random_key exists'
else
puts 'random_key missing'
end
# Output
#
# 'random_key exists'
This is because it returns the string 'method'
, which ruby evaluates to true
in a boolean expression.
hash = {}
defined?(hash['random_key']) # 'method'
The idiomatic Ruby way to check if a key exists in a hash is to use any of the following methods: has_key?
, key?
, include?
, or member?
hash = {}
if hash.include?('random_key')
puts 'random_key exists'
else
puts 'random_key missing'
end
# Output
#
# 'random_key missing'
To learn more about the features and capabilities of a Ruby Hash, check out this article (discussion on Hacker News):

Use parenthesis when using defined?
Since Ruby is so lenient, you don’t always need to use them, but it’s highly recommended due to the low precedence of defined?
keyword.
For example, the following test fails:
class TestDefined < Minitest::Test
def test_precedence
result = 10
assert_equal true, defined? result && result > 0
end
end
# Fails!
# expected: true
# got: "expression"
Adding parentheses results in a better check, and it also results in a clear and readable code. The following test passes with flying colors.
class TestDefined < Minitest::Test
def test_precedence
result = 10
assert_equal true, defined?(result) && result > 0
end
end
That's a wrap. I hope you liked this article and you learned something new. If you're new to the blog, check out the start here page for a guided tour or browse the full archive to see all the posts I've written so far.
As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I receive from developers, and look forward to hearing from you.
If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.