How can I properly chain custom methods in Ruby?

2019-03-19 07:38发布

问题:

I am trying to do a chaining method for the following two methods. After running this code, I kept getting the following output:

#<SimpleMath:0x007fc85898ab70>% 

My question is: what is the proper way of chaining methods in Ruby?

Here is my codes:

class SimpleMath


    def add(a,b=0)
        a + b
        return self
    end


    def subtract(a,b=0)
         a - b
        return self
    end

end
newNumber = SimpleMath.new()
print newNumber.add(2,3).add(2)

回答1:

Are you trying to do something like this?

class SimpleMath
  def initialize
    @result = 0
  end

  #1 add function
  def add(val)
    @result += val
    self
  end

  #2 Subtract function
  def subtract(val)
    @result -= val
    self
  end

  def to_s
    @result
  end
end

newNumber = SimpleMath.new
p newNumber.add(2).add(2).subtract(1)

For any number of arguments

class SimpleMath
  def initialize
    @result = 0
  end

  #1 add function
  def add(*val)
    @result += val.inject(&:+)
    self
  end

  #2 Subtract function
  def subtract(*val)
    @result -= val.inject(&:+)
    self
  end

  def to_s
    @result
  end
end

newNumber = SimpleMath.new
p newNumber.add(1, 1).add(1, 1, 1, 1).subtract(1)


回答2:

Let's define an instance of your class SimpleMath:

sm = SimpleMath.new #=> #<SimpleMath:0x000001020ca820>

Three things to note here:

  • sm is a variable. In Ruby, variables are represented by lower case letters, optionally separated with underscores (e.g., my_var).
  • while it's OK to add () after new, when new has no arguments (aka "parameters"), that's optional and not usually done.
  • if the keyword return is not present, Ruby returns the last calculation performed by the method. Here you would generally write the last line as simply self, and that would be returned. Alas, that matters not, as returning self, with or without the keyword return, is not what you want.

Try the following in IRB:

sm.add(2) #=> #<SimpleMath:0x000001020ca820>

You no doubt were expecting this to return 2+0 #=> 2, but instead it returned self, which, as you can see above, is in fact sm (#<SimpleMath:0x000001020ca820>).

You can fix this by simply removing the line:

return self

from add and subtract:

class SimpleMath
  def add(a,b=0)
    a + b
  end

  def subtract(a,b=0)
    a - b
  end
end

Now

sm = SimpleMath.new
sm.add(2) #=> 2

However, if we try to chain another add, we have another problem:

sm.add(2).add(2,3) #=> NoMethodError: undefined method `add' for 2:Fixnum

This message is very clear: the class Fixnum, of which 2 is an instance, has no instance method named add. That's because you defined it for the class SimpleMath, not for Fixnum.

When Ruby executes sm.add(2).add(3,4), it first computes sm.add(2) #=> 2, which reduces the expression to 2.add(3,4). It then attempts to send the method add (with its two parameters) to 2, but finds the class 2.class #=> Fixnum has no instance method add; hence the exception.

We can correct that error by defining these methods for class Fixnum instead:

class Fixnum
  def add(a,b=0)
    a + b
  end

  def subtract(a,b=0)
    a - b
  end
end

You can confirm that these methods have been added to the Fixnum class by running:

Fixnum.instance_methods.sort

Now, another problem:

sm = Fixnum.new #=> NoMethodError: undefined method `new' for Fixnum:Class

Oh, my, the class Fixnum has no new method! That's because the instances of Fixnum are integers, which cannot be created. You can easily confirm that integers are instances of Fixnum:

72.class #=> Fixnum
-3.class #=> Fixnum

So we can invoke the add method by sending it to any Fixnum instance:

72.add(2) #=> 2
-3.add(2) #=> 2

Now let's try to chain add operations:

72.add(2).add(3,4)       #=> 7
72.add(2000000).add(3,4) #=> 7

No exception, but no chaining. The way to fix this is to change the methods yet again:

class Fixnum
  def add(b=0)
    puts "in add, self = #{self}, b = #{b}"
    self + b
  end

  def subtract(b=0)
    puts "in subtract, self = #{self}, b = #{b}"
    self - b
  end
end

I've added a puts statement in each method in case more debugging is needed. We'll remove these when the code works properly. Let's test:

2.add(3)                    #=> 5
  in add, self = 2, b = 3
5.add                       #=> 5
  in add, self = 5, b = 0
5.add(7)                    #=> 12
  in add, self = 5, b = 7
2.add(3).add.add(7)         #=> 12
  in add, self = 2, b = 3
  in add, self = 5, b = 0
  in add, self = 5, b = 7

2.subtract(5)               #=> -3
  in subtract, self = 2, b = 5
-3.subtract                 #=> -3
  in subtract, self = -3, b = 0
2.subtract(5).subtract      #=> -3
  in subtract, self = 2, b = 5
  in subtract, self = -3, b = 0

2.add(3).subtract(5).add(7) #=>  7
  in add, self = 2, b = 3
  in subtract, self = 5, b = 5
  in add, self = 0, b = 7

Success! Get it?



回答3:

This fellow (tjackiw.tumblr.com) uses this as an interview question and gives a very clean walkthough of how & why the correct answer is similar to the following:

class Interpreter

  def initialize(&block)
    instance_eval(&block)
  end

  def at(time)
    @time = time
    self
  end

  def when(date)
    @date = date
    self
  end

  def we(*people)
    @people = people
    self
  end

  def going(where)
    @where = where
    self
  end

  def output
    [@people.join(' and '), "are going", @where, @date, "at", @time].join(' ')
  end

end


回答4:

Another way is to build pipeline via chainable_methods gem.

Described in the article

require 'chainable_methods'

module SimpleMath
  include ChainableMethods

  def add(a, b=0)
    a + b
  end

  def subtract(a, b=0)
    a - b    
  end
end

SimpleMath.
  chain_from(5).
  add(5).
  add(5).
  subtract(3).
  unwrap