johncorser
johncorser

Reputation: 9822

What is the elegant "Ruby Way" of handling nil values?

So I want to conditionally assign variables based on whether or not the input has been given.

For example

@name = params[:input]['name'] || "Name not yet given"

However, if the params have not been passed yet, this gives an error

method [] does not exist for nil class

I have two ideas to get around this. One is adding a [] method to nil class. Something like:

class NilClass
    def []
        self
    end
end

And the other idea that I have is to use if statements

if params[:input].nil?
    @name = params[:input]['name']
else
    @name = "Name not yet given"
end

However, neither of these solutions feel quite right.

What is the "ruby way"?

Upvotes: 3

Views: 1460

Answers (7)

steel
steel

Reputation: 12520

You could use Hash#dig introduced in Ruby 2.3.0.

@name = params.dig(:input, 'name') || "Name not yet given"

Similarly if you want to gracefully handle nil returns when chaining methods, you can use the safe navigation operator also introduced in Ruby 2.3.0.

@name = object&.input&.name || "Name not yet given"

Upvotes: 1

Cary Swoveland
Cary Swoveland

Reputation: 110675

As I understand, for the hash h, you want to know if

  • h has a key :input and if so
  • h[:input] is a hash and if so
  • h[:input] has a key "name"

If "yes" to all three, return h[:input]["name"]; else return "Name not yet given".

So just write that down:

def get_name(h)
  if (h[:input].is_a? Hash) && h[:input].key?("name") 
    h[:input]["name"]
  else
    "Name not yet given"
  end
end

params = { hat: "cat" }
get_name(params)
  #=> "Name not yet given"

params = { input: "cat" }
get_name(params)
  #=> "Name not yet given"

params =  { input: {} }
get_name(params)
  #=> "Name not yet given"

params =  { input: { "moniker"=>"Jake" } }
get_name(params)
  #=> "Name not yet given"

params =  { input: { "name"=>"cat" } }
get_name(params)
  #=> "cat"

Another way:

def get_name(h)
  begin
    v = h[:input]["name"]
    v ? v : "Name not yet given"
  rescue NoMethodError
    "Name not yet given"
  end
end

Upvotes: 1

ichigolas
ichigolas

Reputation: 7725

Try to fetch the key:

params[:input].try(:fetch, 'name', "Name not yet given")

Assuming you are on rails which seems likely, otherwise you can concatenate fetchs:

params.fetch(:input, {}).fetch 'name', "Name not yet given"

It's a common practice to define params like this:

def input_params
  params.fetch(:input, {})
end

Which reduces the problem to:

input_params[:name] || 'Whatever'

Upvotes: 0

Arup Rakshit
Arup Rakshit

Reputation: 118271

One way is use Hash#fetch.

params[:input].to_h.fetch('name', "Name not yet given")

Upvotes: 6

konsolebox
konsolebox

Reputation: 75488

@name = params[:input].nil? ? "Name not yet given" : params[:input]['name']
  • With current application, .nil? may optionally be excluded.

Also see my solution for recursions: https://stackoverflow.com/a/24588976/445221

Upvotes: 2

pdobb
pdobb

Reputation: 18037

I like to use NullObjects, or, more specifically, Black Hole objects for this sort of thing. Avdi Grimm has blessed us with a great ruby gem for this construct called naught. So for your situation, I'd install the gem and then start by creating my project-specific Null Object:

# add this to a lib file such as `lib/null_object.rb`
require 'naught'

NullObject = Naught.build do |config|
  config.define_explicit_conversions
  config.define_implicit_conversions
  config.black_hole

  if $DEBUG
    config.traceable
  else
    config.singleton
  end
end

Then, include NullObject::Conversions where needed and go to town, confidently!

# my_class.rb
require 'null_object.rb'
include NullObject::Conversions

Maybe(params[:input])["name"].to_s.presence || "Name not yet given"
# => "Name not yet given"

The great thing about this Black Hole approach is that there's no extra steps needed for any additional chaining. You simply chain methods together as long as you want under the (confident) assumption that it will turn out well. Then, at the end you convert the value to the expected type and the explicit conversions will give you a basic version of that back if something in the chain returned nil before you expected it to.

Maybe(params[:thing1])[:thing2][:thing3].map(&:to_i).sum.to_i
# => 0

Or, if you prefer, you can use Actual to convert a Black Hole object back to its actual value:

Actual(Maybe(params[:input])["name"]) || "Name not yet given"

For more on the Null Object pattern, check out Avdi Grimm's post on the subject. All in all it's a great way to gain confidence and stop type checking (and remember, even checking for nil as with .try() is type checking!). Duck typing is supposed to free us from type checking!

Upvotes: 1

Sergio Tulentsev
Sergio Tulentsev

Reputation: 230336

You can always write some code to sweeten your other code.

class Hash
  def deep_fetch(*path)
    path.reduce(self) { |memo, elem| memo ? memo[elem] : nil }
  end
end

params = { input: { name: 'sergio' } }

params.deep_fetch(:input, :name) # => "sergio"
params.deep_fetch(:omg, :lol, :wtf) # => nil

Upvotes: 2

Related Questions