3

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"?

johncorser
  • 9,262
  • 17
  • 57
  • 102

7 Answers7

6

One way is use Hash#fetch.

params[:input].to_h.fetch('name', "Name not yet given")
Matt
  • 68,711
  • 7
  • 155
  • 158
Arup Rakshit
  • 116,827
  • 30
  • 260
  • 317
  • Isn't `fetch` only in `Hash`? – konsolebox Jul 16 '14 at 19:53
  • What if `params = {input: "cat"}`? – Cary Swoveland Jul 16 '14 at 21:20
  • 1
    @CarySwoveland Then Boom! Destroyed. :-) The thing, is OP has a *hash of hash* in this regard, so OP is point is `nil`. In OP's app, I believe, when `:input` has a value, it is actually `{input: {name: "Foo"}}`, otherwise `{input: nil}`. – Arup Rakshit Jul 17 '14 at 04:21
  • You have a point (as does @nicooga). If the key `:input` is expected to have a value that is either a hash or `nil`, and `{input: "cat"}` is passed, best to raise an exception. Boom! Ha, I love it. – Cary Swoveland Jul 17 '14 at 04:58
2
@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

Community
  • 1
  • 1
konsolebox
  • 72,135
  • 12
  • 99
  • 105
  • `params[:input]` may evaluate to `false`. ...Well yes I think that can apply too. – konsolebox Jul 16 '14 at 20:53
  • If `params = { input: "surname" }`, `params[:input].nil? ? "Name not yet given" : params[:input]["name"] #=> "name"`. This is because the expression evaluates to `"surname"["name"]`, invoking the method [String#[\]](http://www.ruby-doc.org/core-2.1.0/String.html#method-i-5B-5D) (with argument `"name"`) on `"surname"`. – Cary Swoveland Jul 17 '14 at 02:45
  • @CarySwoveland Then it would be up for the OP to decide what to do when `not params[:input].is_a? Hash`. We can do a reversed form of the code above with the condition `params[:input].is_a? Hash`, but that would still be assumptive for now. – konsolebox Jul 17 '14 at 13:00
2

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
Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
1

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!

pdobb
  • 17,688
  • 5
  • 59
  • 74
1

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
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
1

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"
steel
  • 11,883
  • 7
  • 72
  • 109
0

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'
ichigolas
  • 7,595
  • 27
  • 50
  • In your second line of code you need to replace your second comma with a left parenthesis. Once you've done so, suppose `params = { input: "cat" }`? – Cary Swoveland Jul 17 '14 at 02:57
  • Done. Taking your assumption in mind, yeah, that smells like trouble. We could do some hacking to work around that, but in the context of a web app you expect `params[:input]` to be a hash **always**. The user needs to be trying to hack your app to get the error, so I guess it's just fine. The first line of code will work anyway. – ichigolas Jul 17 '14 at 03:11
  • I think you're right. See the comment I left on @Arup's answer. – Cary Swoveland Jul 17 '14 at 05:00