Ruby by default does not include the method []
for NilClass
For example, to check if foo["bar"]
exists when foo
may be nil, I have to do:
foo = something_that_may_or_may_not_return_nil
if foo && foo["bar"]
# do something with foo["bar"] here
end
If I define this method:
class NilClass
def [](arg)
nil
end
end
Something like that would make this possible, even if foo
is nil:
if foo["bar"]
# do something with foo["bar"]
end
Or even:
if foo["bar"]["baz"]
# do something with foo["bar"]["baz"] here
end
Question:
Is this a good idea or is there some reason ruby doesn’t include this functionality by default?
EDIT July 2017: My core complaint was the unwieldy foo && foo["bar"] && foo ["bar"]["baz"]
idiom required to access nested values when at any point the value may be nil
. As of Ruby 2.3, Array#dig and Hash#dig exist, which addresses my concern. Now I can use the following idiom!
if foo && foo.dig("bar", "baz")
# do something
end
# Note that errors will still occur if a value isn't what you expect. e.g.
foo = { "a" => { "b" => "c" } }
foo && foo.dig("a", "b", "c") # raises TypeError: String does not have #dig method
9
Nullable values must be addressed carefully, it’s not the same a variable that always has a certain type than other which sometimes has this type, sometimes is null. Functional languages are usually the most careful on this regard and they have custom types to manage it (Maybe
in Haskell, Option
in Ocaml/F#/Scala, …).
In Ruby I wouldn’t extend the NilClass
as you propose, it may lead to buggy code (note that you’re modifying a basic object used by every library, it’s a disaster waiting to happen). However, it’s understandable that you want some pattern to succinctly interact with values that may be nil
to avoid littering the code with conditionals.
One option is the maybe
proxy pattern proposed in Ick:
hash.maybe[key1].maybe[key2].maybe[key3]
Or the block syntax:
hash.maybe { |h| h[key1][key2][key3] }
3
In some ruby code that I’ve had to tweak, I have added isFalse or something of that nature to the nil object to make some parts be not unhappy with each other.
Open classes are not my favorite thing and making such a change to the nil class is not a small change. It is a significant change because it can change the entire functioning of the entire system.
You can do it. Make sure you regression test everything and document up front all changes to core objects so that other people don’t become surprised when suddenly nil behaves in an unexpected way.
As to why that isn’t default functionality – that is the realm of speculation into the mind of Matz.
1
If this is for a specific condition, and I suspect it is, then delegate the behavior. Check that the object is defined, and then check if it is nil, and work accordingly.
It will preserve the expected behavior in core, and give you the specific changed behavior when you want it.
If later you need to modify the behavior to include False, you have one place to go to account for the needed change.
Changing core behavior for a specific need should always be considered carefully.
Why does something_that_may_or_may_not_return_nil
have to return nil
? And if you don’t want to change that method, just wrap it in another method that returns an empty hash if the wrapped method returns nil
.
As for nesting, you can use Hash
‘s default_proc
to return empty hashes when a key isn’t found:
# self-referential, to provide arbitrary depth
default_proc = ->(h,k){ Hash.new(&default_proc) }
some_hash.default_proc = default_proc
The above lets you access deeply undefined entries without making any changes. But you also need to use empty?
instead of treating the result as a simple boolean.
some_hash[:a][:b][:c].empty?
If you want a solution that will also store those new empty hashes into the parent, use this default_proc
instead:
default_proc = ->(h,k){ h[k] = {}.tap{|newh| newh.default_proc = default_proc } }
# This will auto-vivify:
some_hash = {}
some_hash.default_proc = default_proc
some_hash[:a][:b][:c]
some_hash # {:a=>{:b=>{:c=>{}}}}
Also, Rails provides the try
method, that only calls the named method if the receiver isn’t nil
. Yeah, it’s a lot uglier.
foo.try(:[], 'bar').try(:[], 'baz')
It might be better to wrap the arbitrarily-deep hash into a class that hides away the ugliness. For example, does the ['bar']['baz']
entry have some real-world meaning? If so, you can put a method on that class and have that method reach into that depth without the caller needing to worry about it.
I’d recommend you use Hash#fetch to access hash entries when you’re unsure they’ll be present. This way if the requested key is not found you’ll get a KeyError
which you can handle appropriately. When you blindly access nonexistent keys you get subtly confusing errors that wind up with you trying to act on unexpected nils in odd places elsewhere in your code.
> { a: { b: 1 } }[:a][:z].to_s
=> ""
> { a: { b: 1 } }.fetch(:z).fetch(:b).to_s
KeyError: key not found: :z
Aside: I developed my taste for Hash#fetch
after watching Avdi Grimm’s RubyTapas Episode #8. I pay money for that and so far it’s worth it.
I wrote a gem designed for just this case. I came across this question when looking for a solution, so I thought I’d post here on what I ended up with.
Them gem is called Dottie and is available at https://github.com/nickpearson/dottie.
Here’s a quick sample of how it’s designed to be used:
car = {
'type' => {
'make' => 'Tesla',
'model' => 'Model S'
}
}
# normal Hash access assumes a certain structure
car['type']['make'] # => "Tesla"
car['specs']['mileage'] # => # undefined method `[]' for nil:NilClass
# Dottie lets you reach deep into a hash with no assumptions
d = Dottie(car)
d['type.make'] # => "Tesla"
d['specs.mileage'] # => nil
There’s lots more documentation on the GitHub project page. Hope this helps someone.