Ruby provides a handy defined?(arg)
keyword that returns a string describing its argument.
language = 'Ruby'
defined? language # 'local-variable'
defined? @instance_var # 'instance-variable'
defined? @@class_var # 'class variable'
defined? nil # 'nil'
defined? 1 # 'expression'
defined? 'Ruby' # 'expression'
defined? String # 'constant'
You can use defined?
to test if the expression
refers to anything recognizable. The expression
can be an object, a variable, a method name, etc.
Note that a variable set to nil
is still initialized and recognized by ruby as a local-variable
.
framework = nil
defined?(framework) # 'local-variable'
However, if you pass an undefined variable, defined?
returns nil
. Similarly, if Ruby can’t resolve the expression, it returns nil
.
defined?(some_random_variable) # nil
With the basics out of the way, let's look at a common usage pattern of this keyword.
Usage: Lazy-Evaluation + Caching 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
puts t.result
# Output
#
# heavy calculation here... should happen only once
# 100
# 100
# 100
As you can see, calling the result
method multiple times didn't call the expensive calculate_result
method, since we had cached the result after the first call.
It works... most of the time.
If you have an expensive 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
After the first call, @result
was false
, a valid return value. Still, in later executions, because it evaluated to false
, the Ruby interpreter called the expensive calculate_result
method, which was unnecessary.
In such cases, the defined?
method comes in handy.
Let's 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, skipping the expensive operation.
Don’t use defined? to check hash keys
A common mistake is to use defined?
to verify if a hash key is defined. Although Ruby devs might think it strange, this is especially common if you also write PHP on the side (like me - a big fan of Laravel) and are used to the isset()
method.
Consider the following example.
hash = {}
if defined?(hash['random_key'])
puts 'random_key exists'
else
puts 'random_key missing'
end
# Output
#
# 'random_key exists'
This is because the condition returned 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):
Should You use parenthesis with defined?
Since Ruby is so forgiving, you don’t always need to use parenthesis with defined?
, 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 found this article helpful and you learned something new.
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 get from developers, and I look forward to hearing from you.
If you'd like to receive future articles directly in your email, please subscribe to my blog. Your email is respected, never shared, rented, sold or spammed. If you're already a subscriber, thank you.