7

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
  • Nil, or undefined? If you haven't explicitly said foo = nil or otherwise referenced foo, and you call if foo["bar"], you're going to get an Undefined Method Error. Undefined stuff isn't automatically nil.
    – KChaloux
    Commented Oct 19, 2012 at 19:41
  • That's true. I was assuming that foo has been set somewhere. If it's undefined, you still have the issue. But by defining [], you have one less issue. Commented Oct 19, 2012 at 19:43
  • It doesn't sound like an awful idea, but I'd be careful. Define it somewhere obvious, and make sure the behavior is consistent throughout. I never do much monkeypatching though, so I'm not speaking from experience.
    – KChaloux
    Commented Oct 19, 2012 at 19:44
  • This smells like Perl autovivification. There's many fine caveats there, and you should probably find out which before changing a global behaviour of your language just in order to be lazy. Commented Oct 19, 2012 at 19:52
  • 1
    @Silasj My point was what if some other library should throw an exception in this case but no longer does because array access on nil now returns nil? Sometimes you want that NoMethodError.
    – rlperez
    Commented Nov 2, 2012 at 15:13

6 Answers 6

3

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
  • @JamonHolmgren: Well, yeah, it's a matter of taste, h.maybe[k1].maybe[k2] vs h && h[k1] && h[k1][k2]. The first looks more declarative to me. On the downsides, yeah, the one you mention is probably the more important, a library may depending on a NoMethod exception being raised in that situation. IMO it would also lead to buggy code, it may shadow problems (i.e. I miswrite a key but no exception is raised).
    – tokland
    Commented Oct 19, 2012 at 22:22
  • @Jamon: of course, but the difference is you're being explicit by using "maybe". When you know the hash has those keys, you would simply write h[k1][k2][k3], if you patch NilClass you get always the "promiscous" behavior, you have zero choice.
    – tokland
    Commented Oct 20, 2012 at 20:51
  • @JamonHolmgren Some time ago I've read blog.sharetribe.com/2014/06/02/… Commented Jul 2, 2014 at 10:06
1

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
  • It's not really too much of an issue, I just wondered why, with ruby's penchant for clean code, things like this: if foo && foo["bar"] && foo["bar"]["baz"] && foo["bar"]["baz"]["bang"] are necessary to find out if foo["bar"]["baz"]["bang"] exists when any of the preceding keys could be nil. Commented Oct 19, 2012 at 21:11
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.

1

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.

0

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.

0

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.