0

I'm trying to learn Ruby. I want to pass an arbitrary function and an arbitrary list of arguments and keyword arguments into another function.

for example, I have this arbitrary function below

def dummy_func(a, b)
   return a+b
end

And I have this wrapper function

def wrapper(func, *args, **kwargs)
    func(args, kwargs))
end

I want it so I can pass my arguments in any of the following ways and still return the correct answer

wrapper(dummy_func, a=1, b=2)
wrapper(dummy_func, 1, b=2)
wrapper(dummy_func, a=1, b=2)
wrapper(dummy_func, 1, 2)

Is this possible in Ruby? What would be an idiomatic way of approaching it?

Jackson
  • 365
  • 2
  • 10
  • There's no idiomatic way of doing this, because it's not a very good idea. You're producing a function that is difficult to implement, difficult to consume, and difficult to test, for no real benefit. What if somebody invokes `dummy_func(1, 2, a: 3, b: 4)`? You should have exactly one way of specifying each argument, not two. – user229044 Oct 24 '18 at 16:29
  • In Ruby `f(a=1, b=2)` does not pass in arguments named `a` and `b` like in Python. It assigns values to variables named `a` and `b` while passing those arguments in as if `f(1,2)`. The keyword argument notation is `f(a: 1, b: 2)`. What's the purpose of this wrapper function? It seems like it's going to create a whole lot of ambiguity without providing any real benefits. – tadman Oct 24 '18 at 16:38

2 Answers2

2

The idiomatic way is to instead yield to a block.

def dummy_func(a, b, key:)
   return a+b+key
end

def wrapper
  puts yield
end

a = 4
b = 5
c = 6
wrapper do
  dummy_func(a ,b, key: c)
end

Since the block is closure it can see all the same variables that the call to wrapper can. Now there's no need to pass wrapper's arguments through.


If you really want to make your wrapper, you can do some introspection to determine what arguments the wrapped function takes.

def dummy_func(a, b=23, key: 42)
   return a+b+key
end

def no_keys(a, b=23)
   return a+b
end

def wrapper(func, *array, **hash)
  method = self.method(func)
  takes_array = method.parameters.any? { |p| [:req, :opt, :rest].include?(p[0]) }
  takes_hash = method.parameters.any? { |p| [:keyreq, :key, :keyrest].include?(p[0]) }
  
  if takes_array && takes_hash
    self.send(func, *array, **hash)
  elsif takes_array
    self.send(func, *array)
  elsif takes_hash
    self.send(func, **hash)
  else
    self.send(func)
  end
end

a = 4
b = 5
c = 6
puts wrapper(:dummy_func, a, b, key:c)
puts wrapper(:no_keys, a, b)

But this is quite a bit more complex and less flexible than yielding to a block. This also limits you to "functions" which are really methods on the main object (there are no function references in Ruby). That's why they're called with self.send. Blocks require no assumptions about what is being wrapped.

Karmie
  • 387
  • 2
  • 10
Schwern
  • 153,029
  • 25
  • 195
  • 336
0

The closest you can get is keyword arguments:

https://www.justinweiss.com/articles/fun-with-keyword-arguments/

def hello_message(greeting, time_of_day, first_name:, last_name:)
  "#{greeting} #{time_of_day}, #{first_name} #{last_name}!"
end

args = ["Morning"]
keyword_args = {last_name: "Weiss"}

hello_message("Good", *args, first_name: "Justin", **keyword_args)
=> "Good Morning, Justin Weiss!"
Mark
  • 6,112
  • 4
  • 21
  • 46