Reputation: 34884
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
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
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
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
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