tags:

views:

265

answers:

5

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?

+12  A: 

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) }
johannes
+1 - This is very good, I hadn't thought of using inject
DanSingerman
although, can I use this if the methods need to take arguments?
DanSingerman
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
I would upvote this twice if I could.
vise
A: 

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()
demas
I don't really see how this helps. Can you expand please?
DanSingerman
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 ?
demas
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
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
A: 

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.
my_object.method_a.method_b is not equivalent to my_object.method_a my_object.method_b
DanSingerman
Ah. I guess I was thinking more in terms of my_object.method_a!, etc.
Alison R.
+2  A: 

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
filtering on scopes was exactly the use case where this problem occurred for me.
DanSingerman
To directly apply params to conditions, the following snippet can be useful: Person.all(:conditions => params.slice(:country, :age))
hurikhan77
That's a neat trick if things map perfectly!
tadman
+1  A: 

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
That's rails, rather than vanilla ruby though isn't it?
DanSingerman
Actually rails use `returning`, `tap` is from pure Ruby 1.8.7 and 1.9
MBO
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