24

Is there a good way to chain methods conditionally in Ruby?

What I want to do functionally is

if a && b && c
 my_object.some_method_because_of_a.some_method_because_of_b.some_method_because_of_c
elsif a && b && !c
 my_object.some_method_because_of_a.some_method_because_of_b
elsif a && !b && c
 my_object.some_method_because_of_a.some_method_because_of_c

etc...

So depending on a number of conditions I want to work out what methods to call in the method chain.

So far my best attempt to do this in a "good way" is to conditionally build the string of methods, and use eval, but surely there is a better, more ruby, way?

DanSingerman
  • 36,066
  • 13
  • 81
  • 92
  • 2
    I wonder why more people aren't interested in conditional chaining. It would clean up code quite a bit. – Kelvin Apr 11 '12 at 17:44

10 Answers10

36

You could put your methods into an array and then execute everything in this array

l= []
l << :method_a if a
l << :method_b if b
l << :method_c if c

l.inject(object) { |obj, method| obj.send(method) }

Object#send executes the method with the given name. Enumerable#inject iterates over the array, while giving the block the last returned value and the current array item.

If you want your method to take arguments you could also do it this way

l= []
l << [:method_a, arg_a1, arg_a2] if a
l << [:method_b, arg_b1] if b
l << [:method_c, arg_c1, arg_c2, arg_c3] if c

l.inject(object) { |obj, method_and_args| obj.send(*method_and_args) }
Mike Slinn
  • 7,705
  • 5
  • 51
  • 85
johannes
  • 7,262
  • 5
  • 38
  • 57
  • although, can I use this if the methods need to take arguments? – DanSingerman Nov 25 '09 at 15:20
  • 2
    I don't think this will work, as the result of obj.send replaces the accumulator in the loop, which is probably then not a valid object to send the requested method to on the next run. Easy workaround: explicitly return "obj". – hurikhan77 Mar 21 '10 at 12:35
  • Good answer. I would write the assignment this way, though: l = [(:method_a if a), (:method_b if b), (:method_c if c)].compact – tokland Dec 15 '10 at 15:30
13

You can use tap:

my_object.tap{|o|o.method_a if a}.tap{|o|o.method_b if b}.tap{|o|o.method_c if c}
MBO
  • 30,379
  • 5
  • 50
  • 52
  • That's rails, rather than vanilla ruby though isn't it? – DanSingerman Nov 27 '09 at 15:38
  • 1
    Actually rails use `returning`, `tap` is from pure Ruby 1.8.7 and 1.9 – MBO Nov 27 '09 at 16:23
  • Brilliant - I think this is the best way to achieve what I want. Plus in 1.8.6 you can easily monkey patch it to define the tap method (which I just tried, and seemed to work fine) – DanSingerman Nov 27 '09 at 16:37
  • Have retracted the correct answer as I don't think this actually works now I have tried it in some new code – DanSingerman Dec 06 '10 at 16:42
  • 2
    @DanSingerman Yes, it won't work if your methods return different object instead of modifying `my_object` internally. I should have writen it, but I got your comment that it's what you wantend then – MBO Dec 08 '10 at 09:18
  • @MBO yes you are right. Quite a subtle distinction that confused me. – DanSingerman Dec 08 '10 at 14:22
  • Sorry, but this is flawed solution. This does not work as chaining at all. On the other hand, if the methods change the object internally, than it is just overcomplicated and you can simply write `my_object.method_a if a; my_object.method_b if b; my_object.method_c if c` (obviously on separate lines, but the formating in comments is limited) – gorn Oct 07 '16 at 08:23
  • @gorn First: this answer is 7 years old! Second: question was about chaining, everyone knows that you could just put 3 statements sequentially, but OP was asking if you could chain them. Third: it's not what OP asked for, see: http://stackoverflow.com/questions/1797189/conditional-chaining-in-ruby/1807712?noredirect=1#comment1684916_1797281 – MBO Oct 10 '16 at 09:31
5

Sample class to demonstrate chaining methods that return a copied instance without modifying the caller. This might be a lib required by your app.

class Foo
  attr_accessor :field
    def initialize
      @field=[]
    end
    def dup
      # Note: objects in @field aren't dup'ed!
      super.tap{|e| e.field=e.field.dup }
    end
    def a
      dup.tap{|e| e.field << :a }
    end
    def b
      dup.tap{|e| e.field << :b }
    end
    def c
      dup.tap{|e| e.field << :c }
    end
end

monkeypatch: this is what you want to add to your app to enable conditional chaining

class Object
  # passes self to block and returns result of block.
  # More cumbersome to call than #chain_if, but useful if you want to put
  # complex conditions in the block, or call a different method when your cond is false.
  def chain_block(&block)
    yield self
  end
  # passes self to block
  # bool:
  # if false, returns caller without executing block.
  # if true, return result of block.
  # Useful if your condition is simple, and you want to merely pass along the previous caller in the chain if false.
  def chain_if(bool, &block)
    bool ? yield(self) : self
  end
end

Sample usage

# sample usage: chain_block
>> cond_a, cond_b, cond_c = true, false, true
>> f.chain_block{|e| cond_a ? e.a : e }.chain_block{|e| cond_b ? e.b : e }.chain_block{|e| cond_c ? e.c : e }
=> #<Foo:0x007fe71027ab60 @field=[:a, :c]>
# sample usage: chain_if
>> cond_a, cond_b, cond_c = false, true, false
>> f.chain_if(cond_a, &:a).chain_if(cond_b, &:b).chain_if(cond_c, &:c)
=> #<Foo:0x007fe7106a7e90 @field=[:b]>

# The chain_if call can also allow args
>> obj.chain_if(cond) {|e| e.argified_method(args) }
Kelvin
  • 20,119
  • 3
  • 60
  • 68
4

Although the inject method is perfectly valid, that kind of Enumerable use does confuse people and suffers from the limitation of not being able to pass arbitrary parameters.

A pattern like this may be better for this application:

object = my_object

if (a)
  object = object.method_a(:arg_a)
end

if (b)
  object = object.method_b
end

if (c)
  object = object.method_c('arg_c1', 'arg_c2')
end

I've found this to be useful when using named scopes. For instance:

scope = Person

if (params[:filter_by_age])
  scope = scope.in_age_group(params[:filter_by_age])
end

if (params[:country])
  scope = scope.in_country(params[:country])
end

# Usually a will_paginate-type call is made here, too
@people = scope.all
tadman
  • 208,517
  • 23
  • 234
  • 262
4

Use #yield_self or, since Ruby 2.6, #then!

my_object.
  then{ |o| a ? o.some_method_because_of_a : o }.
  then{ |o| b ? o.some_method_because_of_b : o }.
  then{ |o| c ? o.some_method_because_of_c : o }
Epigene
  • 3,634
  • 1
  • 26
  • 31
  • Fantastic, I can finally do queries like I used to in Laravel https://laravel.com/docs/5.6/queries#conditional-clauses but this is even more straightforward – Jonathan Nov 17 '22 at 18:56
3

Here's a more functional programming way.

Use break in order to get tap() to return the result. (tap is in only in rails as is mentioned in the other answer)

'hey'.tap{ |x| x + " what's" if true }
     .tap{ |x| x + "noooooo" if false }
     .tap{ |x| x + ' up' if true }
# => "hey"

'hey'.tap{ |x| break x + " what's" if true }
     .tap{ |x| break x + "noooooo" if false }
     .tap{ |x| break x + ' up' if true }
# => "hey what's up"
Ryo
  • 2,003
  • 4
  • 27
  • 42
1

I use this pattern:

class A
  def some_method_because_of_a
     ...
     return self
  end

  def some_method_because_of_b
     ...
     return self
  end
end

a = A.new
a.some_method_because_of_a().some_method_because_of_b()
ceth
  • 44,198
  • 62
  • 180
  • 289
  • I don't really see how this helps. Can you expand please? – DanSingerman Nov 25 '09 at 14:36
  • I changed my example to illustrate my idea. Or I just didn't understand your question and you want to build list of methods dynamically ? – ceth Nov 25 '09 at 14:49
  • Demas probably intended to imply that you should put the `if a ...` test inside `some_method_because_of_a`, then just call the entire chain and let the methods decide what to do – glenn jackman Nov 25 '09 at 14:50
  • This isn't really generic enough. e.g. If some of the methods in the chain are native ruby methods, I don't really want to have to monkey patch them for this one use case. – DanSingerman Nov 25 '09 at 14:54
1

Maybe your situation is more complicated than this, but why not:

my_object.method_a if a
my_object.method_b if b
my_object.method_c if c
Alison R.
  • 4,204
  • 28
  • 33
1

If you're using Rails, you can use #try. Instead of

foo ? (foo.bar ? foo.bar.baz : nil) : nil

write:

foo.try(:bar).try(:baz)

or, with arguments:

foo.try(:bar, arg: 3).try(:baz)

Not defined in vanilla ruby, but it isn't a lot of code.

What I wouldn't give for CoffeeScript's ?. operator.

nornagon
  • 15,393
  • 18
  • 71
  • 85
  • 2
    I know this is an old answer for an old question, but Ruby does have an equivalent "Safe Navigation" operator now as of Ruby 2.3! It's `&.` (reference this feature issue in the Ruby trunk: https://bugs.ruby-lang.org/issues/11537) – wspurgin May 04 '17 at 15:40
0

I ended up writing the following:

class Object

  # A naïve Either implementation.
  # Allows for chainable conditions.
  # (a -> Bool), Symbol, Symbol, ...Any -> Any
  def either(pred, left, right, *args)

    cond = case pred
           when Symbol
             self.send(pred)
           when Proc
             pred.call
           else
             pred
           end

    if cond
      self.send right, *args
    else
      self.send left
    end
  end

  # The up-coming identity method...
  def itself
    self
  end
end


a = []
# => []
a.either(:empty?, :itself, :push, 1)
# => [1]
a.either(:empty?, :itself, :push, 1)
# => [1]
a.either(true, :itself, :push, 2)
# => [1, 2]