conditional chaining in ruby
Asked Answered
L

10

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?

Lintel answered 25/11, 2009 at 14:18 Comment(1)
I wonder why more people aren't interested in conditional chaining. It would clean up code quite a bit.Fedora
S
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) }
Southerly answered 25/11, 2009 at 14:26 Comment(3)
although, can I use this if the methods need to take arguments?Lintel
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".Stutter
Good answer. I would write the assignment this way, though: l = [(:method_a if a), (:method_b if b), (:method_c if c)].compactWieland
A
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}
Anaphase answered 27/11, 2009 at 9:41 Comment(8)
That's rails, rather than vanilla ruby though isn't it?Lintel
Actually rails use returning, tap is from pure Ruby 1.8.7 and 1.9Anaphase
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)Lintel
Have retracted the correct answer as I don't think this actually works now I have tried it in some new codeLintel
@Lintel 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 thenAnaphase
@Anaphase yes you are right. Quite a subtle distinction that confused me.Lintel
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)Spencerianism
@Spencerianism 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: #1797689Anaphase
R
6

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 }
Racecourse answered 16/2, 2021 at 10:11 Comment(2)
Fantastic, I can finally do queries like I used to in Laravel laravel.com/docs/5.6/queries#conditional-clauses but this is even more straightforwardTenuous
A small update - the blocks can be made even simpler by omitting the block parameter, and using the numbered block parameter(s) instead: then{ a ? _1.some_method_because_of_a : _1 }.Racecourse
F
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) }
Fedora answered 11/4, 2012 at 17:30 Comment(3)
interesting stuff!Henton
I've also come up with #chain_if, comes handy when conditions apply to Rails scope chains.Racecourse
I came here to write this as a solution but you beat me to it! I call mine tap? instead of chain_if. Very nice!Cultus
C
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
Carrara answered 25/11, 2009 at 15:31 Comment(2)
filtering on scopes was exactly the use case where this problem occurred for me.Lintel
To directly apply params to conditions, the following snippet can be useful: Person.all(:conditions => params.slice(:country, :age))Stutter
C
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"
Cardwell answered 1/11, 2017 at 7:0 Comment(1)
Neat if it works - can do then {} as of 2.6Tenuous
R
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()
Radcliff answered 25/11, 2009 at 14:31 Comment(4)
I don't really see how this helps. Can you expand please?Lintel
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 ?Radcliff
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 doSynergist
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.Lintel
M
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
Monomolecular answered 25/11, 2009 at 14:33 Comment(2)
my_object.method_a.method_b is not equivalent to my_object.method_a my_object.method_bLintel
Ah. I guess I was thinking more in terms of my_object.method_a!, etc.Monomolecular
U
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.

Undergrown answered 5/8, 2013 at 22:31 Comment(1)
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: bugs.ruby-lang.org/issues/11537)Mentor
P
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]
Pridemore answered 21/12, 2015 at 22:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.