3

Sometimes while dealing with API responses, I'll end up writing something like:

what_i_need = response["key"]["another key"]["another key 2"]

The problem with that is, it'll throw an error if, say, "another key" is missing. I don't like that. I'd be a lot happier if what_i_need turned up a nil if something along the process broke.

Is there a more elegant solution than:

what_i_need = nil
begin
  what_i_need = response["key"]["another key"]["another key 2"]
rescue Exception => e
end

I also thought about monkey patching NilClass you try to access nil["something"] it would return nil, but I'm not sure if that's the best way to go about it either of if it's possible even.

Andrew Marshall
  • 89,426
  • 20
  • 208
  • 208
dsp_099
  • 5,021
  • 13
  • 59
  • 115

4 Answers4

11

Use Hash#fetch with default value.

h = {:a => 2}
h.fetch(:b,"not present")
# => "not present"
h.fetch(:a,"not present")
# => 2

Without default value it will throw KeyError.

h = {:a => 2}
h.fetch(:b)
# ~> -:2:in `fetch': key not found: :b (KeyError)

But with nested Hash like your one you can use :

h = {:a => {:b => 3}}
val = h[:a][:b] rescue nil # => 3
val = h[:a][:c] rescue nil # => nil
val = h[:c][:b] rescue nil # => nil
Arup Rakshit
  • 109,389
  • 25
  • 234
  • 293
6

Ruby 2.0 has NilClass#to_h.

what_i_need = response["key"].to_h["another key"].to_h["another key 2"]
sawa
  • 156,411
  • 36
  • 254
  • 350
  • @naomik Yeah. It is a cool feature. You can do similar things with arrays like `array[0].to_a[1]`. – sawa Jul 08 '13 at 05:12
  • any reason for the `to_a` and `to_h` names? They don't really seem to fit. I would expect something like `try_a` or `try_h` – Thank you Jul 08 '13 at 05:16
  • 1
    They always succeed. `to_a` always turns something into an array. `to_h` always turns something into a hash. – sawa Jul 08 '13 at 05:24
2

Taking some inspiration from Objective-C's key-value coding system, you can do this with a lightweight DSL to walk a series of keys in an arbitrarily-nested data structure:

module KeyValue
  class << self
    def lookup(obj, *path)
      path.inject(obj, &:[]) rescue nil
    end
  end
end

h = { a: { b: { c: 42, d: [ 1, 2 ] } }, e: "test"}

KeyValue.lookup(h, :a, :b, :d, 1) # => 2
KeyValue.lookup(h, :a, :x)        # => nil

Or if you just want the one-liner:

(["key", "another key", "another key 2"].inject(h, &:[]) rescue nil)
d11wtq
  • 33,252
  • 14
  • 115
  • 186
1

To expand on @Priti’s answer a bit, you can use a chain of Hash#fetch instead of Hash#[] to return empty hashes till you get to the last in the chain, and then return nil:

what_i_need = response.fetch('key', {})
                      .fetch('another key', {})
                      .fetch('another key 2', nil)

or rely on the KeyError exception being raised (maybe not the best, as exceptions as control flow should be avoided):

what_i_need = begin
                response.fetch('key').fetch('another key').fetch('another key 2')
              rescue KeyError
                nil
              end
Community
  • 1
  • 1
Andrew Marshall
  • 89,426
  • 20
  • 208
  • 208
  • If you are using `rescue`, there is no particular reason to switch from `Hash#[]` to `Hash#fetch`, which would make your second solution the same as the OPs. – sawa Jul 07 '13 at 14:12
  • OK. I got you point. You consider rescuing `KeyError` is better than rescuing `Exception`. – sawa Jul 07 '13 at 14:15
  • 2
    @sawa Well, just about anything is better than rescuing from Exception, but even if it was just StandardError there’s a whole multitude of other exceptions that could occur which you probably don’t want to rescue from. – Andrew Marshall Jul 07 '13 at 14:17
  • @AndrewMarshall I do like your `Hash#fetch` implementation. – Arup Rakshit Jul 07 '13 at 15:47
  • That's pretty cool. Why is begin rescue a bad thing in that scenario? What is a good place to use being rescue? – dsp_099 Jul 07 '13 at 17:01
  • @dsp_099 You may wish to see the SO question [Why not use exceptions as regular flow of control?](http://stackoverflow.com/questions/729379/why-not-use-exceptions-as-regular-flow-of-control) for some discussion. I think rescuing immediately isn’t so bad, since it’s obvious where the exception was thrown. Then again what’s that point of using an exception that can bubble up when you immediately rescue it? It’s a matter of design and style and semantics more than functional difference. – Andrew Marshall Jul 07 '13 at 17:57