eiko
eiko

Reputation: 5345

Safe navigation operator (lonely operator) not working for hash

I'm trying to use the safe navigation operator & to optionally call the [] method on a variable which may be a hash or nil. As far as I know, that's what safe navigation operators are for.

my_hash = nil
puts "hello, #{ my_hash&[:test] }"
my_hash = { test: "world" }
puts "hello, #{ my_hash&[:test] }"

I expect this to output:

hello, false
hello, world

But but it does not work with hashes; instead, I get:

hello, false
undefined method `&` for `{:test=>"world"}:Hash` (NoMethodError)

What am I doing wrong?

Upvotes: 3

Views: 2932

Answers (4)

Todd A. Jacobs
Todd A. Jacobs

Reputation: 84393

TL;DR

You've got logic and syntax errors in your program including:

  • Treating nil (an instance of NilClass) like an empty Hash.
  • Misunderstanding what's being returned by interpolation of nil ("").
  • Syntax errors in your use of the safe navigation operator (&.).

In the sections below, I address your logic errors and offer some suggestions for using native Hash methods that accomplish the results your looking for without silently swallowing errors that would have brought more attention to the problematic parts of the code.

The goal of this answer isn't just to point out errors, but to also demonstrate how to accomplish what you're trying to do successfully. Your mileage may vary.

Fixing Your Logic Errors

There's are a couple of key logic error in your original question. If you want to return "false" for interpolation if a Hash value doesn't exist, use #fetch. For example:

hash = {}
hash.fetch :test, false
#=> false

However, there's an important caveat. Hash#fetch will raise a NoMethodError if called on nil, because nil doesn't respond to fetch:

nil.respond_to? :fetch
#=> false

This is the problem safe navigation is designed to handle: a missing method. It therefore makes sense to use safe navigation if you swallow the exception rather than rescue it or handle it with #method_missing. For example:

hash = nil
hash.fetch :test, false
#=> NoMethodError: undefined method `fetch' for nil:NilClass

hash&.fetch :test, false
#=> nil

However, doing this essentially results in silently swallowing an error in the same way that hash.fetch :test rescue false would, so it's generally the wrong thing to do because it hides the fact that there's a logic problem or that you've gotten ahold of an unexpected object like nil rather than an empty hash. However, it would certainly solve your interpolation problem:

hash = nil
"#{hash.fetch :test rescue false}"
#=> "false"

For your posted code, I stress that you probably want to use sensible Hash methods that don't silently swallow exceptions, but your real-world mileage may certainly vary.

Safe Navigator Not Needed; Use Existing Hash Methods

You appear to have at least four other related problems in your original question:

  1. The safe navigation operator is &. so it only works with methods using dotted notation. my_hash&[:test] is not a valid construct for that.
  2. The safe navigator handles objects that lack a given method. However, Hash#[] exists, so even if the parser considered Hash&.[] to be a valid construct (which it doesn't), this wouldn't do what you're expecting anyway.
  3. You probably want Hash#fetch or Hash#values_at instead, depending on the expected structure of your hash and the object you want to return.
  4. When you use string interpolation on nil, you're implicitly calling #to_s. But nil.to_s doesn't do what you think; it returns an empty string, not an instance of FalseClass. For proof, consider that "#{nil}" #=> "".

Examples of Relevant Hash Methods

Consider the following examples:

hash = {}

hash.fetch :test
#=> KeyError: key not found: :test

hash.fetch :test, nil
#=> nil

hash.values_at :test
#=> [nil]

Hash#fetch with a default of nil is probably what you want in most cases, although you can certainly rescue KeyError or rescue nil (or even rescue false) if you prefer to do that, although a catch-all rescue for truly unexpected exceptions is generally considered a code smell.

Upvotes: -3

sawa
sawa

Reputation: 168179

Your problem is that there is no such thing as "the safe navigation operator &" as you assume.

What is called the safe navigation operator is &.. Nowhere in your code:

my_hash&[:test]

is there a safe navigation operator &..

Upvotes: 0

Tom Lord
Tom Lord

Reputation: 28305

I'm trying to use & to optionally call the [] method on a hash which may or may not be nil.

In order to do that, you need to directly invoke the [] method. hash[:key] is actually calling the :[] method on the hash:

# This will not work:
my_hash&[:test]

# This will:
my_hash&.[](:test)

This is clearly not the prettiest solution, though... You may instead want to use a guard clause in the method (return if my_hash.nil?), or do something like this:

my_hash.to_h[:test]

Upvotes: 1

Ursus
Ursus

Reputation: 30071

Because this

my_hash[:test]

is syntactic sugar for this one

my_hash.[](:test)

so this should work

my_hash&.[](:test) 

but it's not pretty, I know.

Upvotes: 8

Related Questions