Ruby: How to chain multiple method calls together with "send"
Asked Answered
S

4

35

There has to be a built in way of doing this, right?

class Object
  def send_chain(arr)
    o=self
    arr.each{|a| o=o.send(a) }
    return o
  end
end
Sorption answered 4/11, 2010 at 17:28 Comment(2)
I'm not sure, but I've gone down paths like this before, and I generally find that they lead to a bad implementation of something. What is it you're actually attempting to accomplish? Maybe there is an existing programming pattern that would benefit you more.Bordelon
Eh, it's just a rake task to output some columns from some ActiveRecord models. It accepts an array of strings, with each string representing either a model's attribute/method or a model's associated model's attribute/method. So, I want to give it something like ["date", "title", "color", "author.name"] for the "Book" model and have it print out the attributes and the associated author's name attribute. I guess I could write out methods for Book that would return self.author.name, but just wanted something quick for this one-off rake output task.Sorption
N
70

I just ran across this and it really begs for inject:

def send_chain(arr)
  arr.inject(self) {|o, a| o.send(a) }
end
Nikethamide answered 22/8, 2012 at 3:8 Comment(3)
Shorter: arr.inject(self, :send)Willianwillie
Safer: arr.inject(self, :try) :)Colic
How can one do this when one of the methods to send takes arguments?Livraison
F
10

Building upon previous answers, in case you need to pass arguments to each method, you can use this:

def send_chain(arr)
  Array(arr).inject(self) { |o, a| o.send(*a) }
end

You can then use the method like this:

arr = [:to_i, [:+, 4], :to_s, [:*, 3]]
'1'.send_chain(arr) # => "555"

This method accepts single arguments as well.

Fionnula answered 2/3, 2016 at 17:39 Comment(0)
S
8

No, there isn't a built in way to do this. What you did is simple and concise enough, not to mention dangerous. Be careful when using it.

On another thought, this can be extended to accept arguments as well:

class Object
  def send_chain(*args)
    o=self
    args.each do |arg|
      case arg
      when Symbol, String
        o = o.send arg # send single symbol or string without arguments
      when Array
        o = o.send *arg # smash the inner array into method name + arguments
      else
        raise ArgumentError
      end
    end
    return o
  end
end

this would let you pass a method name with its arguments in an array, like;

test = MyObject.new
test.send_chain :a_method, [:a_method_with_args, an_argument, another_argument], :another_method
Stoltzfus answered 4/11, 2010 at 17:44 Comment(3)
Just being nitpicky here, but keeping in line with Ruby idioms, this really should be duck-typed.Fabricate
@edgerunner. It didn't worked for me sa.grade_accesses.send_chain(:except_grade_k, :except_grade_math_fact) NoMethodError: undefined method `except_grade_k' for #<Array:0x007fed40a2e0d8>Sharla
@MohitJain all methods should return self.Discriminating
L
0

How about this versatile solution without polluting the Object class:

def chain_try(arr)
  [arr].flatten.inject(self_or_instance, :try)
end

or

def chain_send(arr)
  [arr].flatten.inject(self_or_instance, :send)
end

This way it can take a Symbol, a String or an Array with a mix of both even.🤔

example usage:

  • chain_send([:method1, 'method2', :method3])
  • chain_send(:one_method)
  • chain_send('one_method')

Lascivious answered 27/3, 2020 at 20:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.