Incerteza
Incerteza

Reputation: 34884

Chain method calls in Ruby

Is there an elegant way to "continue if success" in Ruby? Something like this:

method1(a, b)
 .and_then(method2)
 .and_then(method3)
 .fail { |x| 'something is wrong'}

Upvotes: 2

Views: 1178

Answers (5)

Zara Kay
Zara Kay

Reputation: 164

I don't believe there is a way to do it exactly how you have shown, however you could use a begin and rescue block instead.

So the code would look like this:

begin
    method1(a,b).method2.method3
rescue
    p "something is wrong"
end

In any of the three methods, you should raise some sort of exception, simply by calling

raise "Something is Wrong"

and this will stop execution and run the rescue block. If you want to get some sort of data from the execution context where the raise call happened, you will need to implement your own type of error or use the existing types. If you wish to do this the rescue statement would need to be changed as follows

rescue => e

or if you have a type

rescue ArgumentError => e

This is a bit complex so at good article can be found here

This will only work if the methods you call actually raise exceptions.

Upvotes: 5

Drenmi
Drenmi

Reputation: 8777

There is no syntactical sugar like this in Ruby by default, but you can add it to your own classes if a construct like this is beneficial to your project. Take some time to think about whether or not it is a good fit, before implementing it over conventional exception handling.

In the solution below, I have added a flag to the object, indicating if a step failed, and then set it to true if an exception is thrown. At the end of the method chain, the failed? method checks whether it is true, and yields to a block if provided. You can set the flag based on any criteria you would like.

Note that there is no class wide exception handling in Ruby (and probably for good reasons), so you'll need to add handlers to whatever methods you find relevant.

class Thing
  def initialize(name)
    @failed = false
    @name = name
  end

  def change
    begin
      @name.capitalize!
    rescue
      @failed = true
    end
    self
  end

  def failed?
    yield self if @failed && block_given?
    @failed
  end
end

thing = Thing.new(5)
# This will fail, because a Fixnum doesn't respond to capitalize!
thing.change.fail? { puts "Ooops." }
# Ooops.

You can also extract the functionality into a module and use it as a mix-in for any classes that need this behaviour:

module Failable
  @failed = false

  def failed?
    yield self if @failed && block_given?
    @failed
  end

  def fail!
    @failed = true
  end
end

class Thing
  include Failable

  def initialize(name)
    @name = name
  end

  def change
    begin
      @name.capitalize!
    rescue
      fail!
    end
    self
  end
end

Upvotes: 0

Cary Swoveland
Cary Swoveland

Reputation: 110675

By "fail", I assume you mean that one of the chained methods returns an object (e.g., nil) whose methods do not include the following method in the chain. For example:

def confirm_is_cat(s)
  (s=='cat') ? 'cat' : nil
end

Then:

confirm_is_cat('cat').upcase
  #=> "CAT" 
confirm_is_cat('dog').upcase
  # NoMethodError: undefined method `upcase' for nil:NilClass

We'd like to return "CAT" in the first case, but avoid the exception being raised in the second.

One approach is to catch exceptions, which has been mentioned in other answers. Another way that is sometimes seen is to make use of Enumerable#reduce (aka inject). I cannot offer a general solution with this approach, but I'll give an example that should show you the general idea.

Suppose we add four methods to Fixnum. Each method returns nil if the receiver is a particular value (indicated by the method name); else it returns its receiver:

class Fixnum
  def chk0; (self==0) ? nil : self; end
  def chk1; (self==1) ? nil : self; end
  def chk2; (self==2) ? nil : self; end
  def chk3; (self==3) ? nil : self; end
end

We want to start with a given integer n and compute:

n.chk0.chk1.chk2.chk3

but stop the calculation if chk0, chk1 or chk2 returns nil. We can do that as follows:

meths = [:chk0, :chk1, :chk2, :chk3]

def chain_em(meths,n)
  meths.reduce(n) { |t,m| t && t.send(m) }
end

chain_em(meths,0)
  #=> nil 
chain_em(meths,1)
  #=> nil 
chain_em(meths,2)
  #=> nil 
chain_em(meths,3)
  #=> nil 
chain_em(meths,4)
  #=> 4

Now suppose we also want to identify which method returns nil, if any (other than chk3) does. We can modify the method accordingly:

def chain_em(meths,n)
  last_meth = ''
  meths.reduce(n) do |t,m|
    if t.nil?
      puts "#{last_meth} returned nil"
      return nil
    end
    last_meth = t
    t && t.send(m)
  end
end

chain_em(meths,0)
  #-> chk0 returned nil
  #=> nil 
chain_em(meths,1)
  #-> chk1 returned nil
  #=> nil 
chain_em(meths,2)
  #-> chk2 returned nil
  #=> nil 
chain_em(meths,3)
  #=> nil 
chain_em(meths,4)
  #=> 4 

Upvotes: 0

Sergio Tulentsev
Sergio Tulentsev

Reputation: 230336

You can do this if you're willing to keep an open mind (think of method return value being either a Success or a Failure)

https://github.com/pzol/deterministic

def works(ctx)
  Success(1)
end

def breaks(ctx)
  Failure(2)
end

def never_executed(ctx)
  Success(99)
end

Success(0) >> method(:works) >> method(:breaks) >> method(:never_executed) # Failure(2)

It is even almost possible to use your exact pseudo-syntax from the question!

require 'deterministic'

method1 = proc { |a, b| Deterministic::Result::Success(a * b) }
method2 = proc { Deterministic::Result::Success(2) }
method3 = proc { Deterministic::Result::Failure(3) } # error here, or whatever
error_handler = proc { |x| puts 'something is wrong'; Deterministic::Result::Success('recovered') }

res = method1.call(5, 6).
           and_then(method2).
           and_then(method3).
           or_else(error_handler)
res # => Success("recovered")
# >> something is wrong

Upvotes: 1

shivam
shivam

Reputation: 16506

Using tap you can “tap into” a method chain, in order to perform operations on intermediate results within the chain. Inside the tap block you can place your conditions.

 method1(a, b).tap{|o| o.method2 if cond1}.tap{|o|o.method3 if cond2}

Upvotes: 4

Related Questions