2

I have a method that looks like this:

def calculate_the_thing(hsh)
  hsh[:x] + hsh[:y] + hsh[:z]
end

which takes something like this:

{:x => 5, :y => nil, :z => 2, :a => 5}

I'd like to patch some classes so that when the + method gets a nil value, it treats it as zero. Seems reasonable. How might I do that?

sawa
  • 165,429
  • 45
  • 277
  • 381
boulder_ruby
  • 38,457
  • 9
  • 79
  • 100
  • 5
    nil.to_i already coerces to 0. So by using to_i on your inputs you get your desired behaviour by default. Defining nil to be equal to 0 in all contexts seems quite evil. – jforberg Feb 06 '16 at 16:35
  • didn't know you could do that. That's a huge help – boulder_ruby Feb 06 '16 at 18:30

2 Answers2

7

As @jforberg points out, you can just use the #to_i method which will return 0 for nil.

def calculate_the_thing(hsh)
  hsh[:x].to_i + hsh[:y].to_i + hsh[:z].to_i
end

Alternatively, you can also define the hash with an automatic default value...

hsh = Hash.new{0}

But if you have a method that explicitly puts nil as a hash value that will override the default value.

SteveTurczyn
  • 36,057
  • 6
  • 41
  • 53
5

You need to monkey-patch Nilclass to coerce nil to an integer. Here is the code:

class NilClass
  def coerce(n)
    return [0, n] if n.is_a? Numeric
    super
  end
end

1 + nil
#=> 1

Have a look at this thread - In Ruby, how does coerce() actually work? - to understand the concept of coerce.


However, there is an issue with above code:

nil + 1
#=> undefined method `+' for nil:NilClass (NoMethodError)

To fix this problem, you will have to define + method on NilClass

class NilClass
  def +(param)
    param  + self
  end
end

nil + 1
#=> 1

If we try to get adventurous and try:

nil * 10
#=> undefined method `*' for nil:NilClass (NoMethodError)

By being adventurous, lets handle all such undefined methods by implementing our own method_missing handler.

class NilClass
  def method_missing m, *args, &block
    args.first.send(m, self, &block) if args.size == 1
  end
end

p nil * 1
#=> 0

Next, lets try:

nil + "hello"
# '+': can't convert NilClass to String (NilClass#to_str gives NilClass) (TypeError)

Lets fix this as well

class NilClass
  def to_str
    ""
  end
end

nil + "hello"
#=> "hello"

Next, lets try this:

nil + [1,2,3]
#=> '+': can't convert NilClass to Array (NilClass#to_ary gives NilClass) (TypeError)

Lets fix it:

class NilClass
  def to_ary
    []
  end
end

nil + [1,2,3]
#=> [1, 2, 3]

We now have this version of NilClass:

class NilClass
  def coerce(n)
    return [0, n] if n.is_a? Numeric
    super
  end

  def method_missing m, *args, &block
    args.first.send(m, self, &block) if args.size == 1
  end

  def to_str
    ""
  end

  def to_ary
    []
  end
end

Caution:: Above code shows that what you want to do can be done. However, this should be used only for experimental and learning purpose. It is really not feasible to make nil behave like other operand in all operations and you will end up monkey-patching NilClass to no end. Hence, its better to stay off from this kind of monkey patching to avoid scary surprises to future Rubyists who will be maintaining your code.

Community
  • 1
  • 1
Wand Maker
  • 18,476
  • 8
  • 53
  • 87
  • 2
    In almost all cases, this is a *really* bad idea as it goes against fundamental assumptions in ruby and can lead to hard to find bugs. Patching a core-class in that way should be avoided at all costs. The only way this could be use kind of safe is when using [Refinements](http://ruby-doc.org/core/doc/syntax/refinements_rdoc.html). – Holger Just Feb 06 '16 at 17:41
  • @HolgerJust Yeah, added a caution note to the answer – Wand Maker Feb 06 '16 at 17:45