conditional chaining in ruby

2019-01-22 03:50发布

问题:

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?

回答1:

You could put your methods into an arry 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) }


回答2:

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}


回答3:

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


回答4:

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) }


回答5:

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


回答6:

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()


回答7:

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.



回答8:

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"


回答9:

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]


标签: ruby chaining