Ruby programmatically calling method, with variable number of args
Asked Answered
H

3

8

I am trying to do something similar to this:

def foo(mode= :serial)

  if (mode == :serial) then
    self.send(:bar, "one_type")
  else
    self.send(:bar,"second_type",:T5)
  end
end

I can obviously type this out like this.

But I've recently tried expanding it to include a second function like this:

def foo(mode= :serial)

  if (mode == :serial) then
    self.send(:bar, "one_type")
    self.send(:foobar, "one_type",20)
  else
    self.send(:bar,"second_type",:T5)
    self.send(:foobar, "one_type",20,:T5)
  end
end

I can still continue as it is, but I thought to myself, there's a pattern here, I should abstract the arguments away into another layer and make this simpler.

So what I wanted to do was something like this:

arg_arr   = [:bar, "one_type"]
arg_arr_2 = [:foobar, "one_type", 20]
if (mode == :serial) then
  self.send(arg_arr)
  self.send(arg_arr_2)
else
  arg_arr << :T5
  arg_arr2 << :T5
  self.send(arg_arr)
  self.send(arg_arr2 )
end

I tried some other ideas involving .each, .inspect, but nothing that would work (the usual error was can't convert array into string, which I'm guessing refers to the fact that it treats the array as the entire function name). I can do it if I explicitly say "use array elements[0] , [1] etc, but that just seems wasteful.

Is there a way to achieve this without writing code that is hardcoded to the number of arguments?

Hogarth answered 11/1, 2017 at 8:5 Comment(0)
B
6

Try this

def foo(a, b)
  puts a
  puts b
end

array = ['bar', 'qux']
send(:foo, *array) # using send
foo(*array) # using the method name

Both print

bar
qux

The splat operator * packs or unpacks an array.

Bajaj answered 11/1, 2017 at 8:35 Comment(0)
T
3

Some years ago I did what you are trying now. With an asterisk in front of a method parameter you can receive as many parameters as you want in a function. So You don't need to know the number of the given parameters. It's called a splat.

Send your values as an array with an asterisk in front too and it will work.

I tested the folling with an irb console:

def test(*args)
  puts args.inspect
end

my_args = [1, 2, 3]
self.send(:test, *my_args)
# [1, 2, 3]
# => nil

Or send as many single parameters as you want:

self.send(:test, 'a', 'b', 'c', 'd')
# ["a", "b", "c", "d"]
# => nil

If you have a fixed number of parameters this will work:

def test(arg1, arg2, arg3)
  puts arg1.inspect
  puts arg2.inspect
  puts arg3.inspect
end

my_args = [1, 2, 3]
self.send(:test, *my_args)
# 1
# 2
# 3
# => nil
Truffle answered 11/1, 2017 at 8:36 Comment(2)
This solution works. I can't help to ask, is what I'm doing a good practice? Or do you think I should construct this differently?Hogarth
I'm rails programmer for more than 7 years and for me it's no fault to do it this way. I saw a lot of rails code using splats. Be careful to not send direct user input to your function. This could be a security vulnerability.Truffle
K
3

First, you shouldn't use send. You could use public_send, Method#call or just bar(...) if you know the method name.

Homogenous parameters

If the parameters are homogenous (e.g. are instances of the same Class), you can just put them in an Array, and use this Array as parameter :

def analyze_array(array)
  puts "Elements : #{array}"
  puts "Length   : #{array.size}"
  puts "Sum      : #{array.inject(:+)}"
  puts
end

analyze_array([1,2,3])
analyze_array([1,2,3,4,5])

It outputs :

Elements : [1, 2, 3]
Length   : 3
Sum      : 6

Elements : [1, 2, 3, 4, 5]
Length   : 5
Sum      : 15

Example

Refactoring your code a bit, it could become :

arg_arr   = [:bar, 1]
arg_arr_2 = [:foobar, 1, 2]

def bar(array)
  puts "  bar with one parameter : #{array}"
end

def foobar(array)
  puts "  foobar with one parameter : #{array}"
end

[:serial, :parallel].each do |mode|
  puts "Mode : #{mode}"
  [arg_arr, arg_arr_2].each do |method_and_args|
    method_name, *args = method_and_args
    args << 3 if mode != :serial
    method(method_name).call(args)
  end
end

It outputs :

Mode : serial
  bar with one parameter : [1]
  foobar with one parameter : [1, 2]
Mode : parallel
  bar with one parameter : [1, 3]
  foobar with one parameter : [1, 2, 3]

Heterogenous parameters

For an unknown number of parameters that might belong to different classes, you can use the splat operator (documentation) :

def analyze_parameters(*params)
  puts "Parameters : #{params}"
  puts "Number     : #{params.size}"
  puts "Classes    : #{params.map(&:class)}"
end

analyze_parameters('Test')
analyze_parameters(1, 'a', :b, [:c, :d])

It outputs :

Parameters : ["Test"]
Number     : 1
Classes    : [String]

Parameters : [1, "a", :b, [:c, :d]]
Number     : 4
Classes    : [Fixnum, String, Symbol, Array]

Your example becomes :

arg_arr   = [:bar, 1 ]
arg_arr_2 = [:foobar, 1, 'a']

def bar(*params)
  puts "  bar with multiple parameters : #{params}"
end

def foobar(*params)
  puts "  foobar with multiple parameters : #{params}"
end

[:serial, :parallel].each do |mode|
  puts "Mode : #{mode}"
  [arg_arr, arg_arr_2].each do |method_and_args|
    method_name, *args = method_and_args
    args << :b if mode != :serial
    method(method_name).call(*args)
  end
end

It outputs :

Mode : serial
  bar with multiple parameters : [1]
  foobar with multiple parameters : [1, "a"]
Mode : parallel
  bar with multiple parameters : [1, :b]
  foobar with multiple parameters : [1, "a", :b]
Klopstock answered 11/1, 2017 at 9:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.