ulferts
ulferts

Reputation: 2242

How to pass on all parameters of a method send

Question

When in a method, how can I pass on all parameters provided as part of the method sending to another method sending? The parameters should all be named in the sense that there are no placeholders/catchalls like *args but they can be a mix of keyword arguments and non keyword arguments. Additionally, the inner method send is not super.

Example (pseudocode)

def some_method(a_parameter, a_named_parameter:)
  ...do something...
  some_other_method([send with original parameters])
  ...do something else...
end

Related question

Is there a way to access method arguments in Ruby? has been asked 7 years ago.

Ugly hack

Based on what I have found, it is possible to do something like this for keyword parameters:

def some_method(a_named_parameter:, another_named_parameter:)
  ...do something...

  params = method(__method__)
           .parameters
           .map { |p| [p[1], binding.local_variable_get(p[1])] }
           .to_h

  some_other_method(**params)
  ...do something else...
end

And this for non-keyword parameters:

def some_method(a_named_parameter, another_named_parameter)
  ...do something...

  params = method(__method__)
           .parameters
           .map { |p| binding.local_variable_get(p[1]) }

  some_other_method(*params)
  ...do something else...
end

Based on the information returned by method(__method__).parameters one could also work out a solution that would work for both, but even given that it would be possible to extract all of this into a helper, it is way to complicated.

Upvotes: 1

Views: 695

Answers (3)

engineersmnky
engineersmnky

Reputation: 29308

This was a very interesting question so first of all, thank you for that.

Given that Binding is an object, like any other, we can build a class to provide this type of functionality and leverage the binding from the original method to delegate all the arguments to the next method like so:

class ArgsBuilder 
  attr_reader
  def initialize(b)
    @standard_args, @kwargs = [],{}
    @binding = b
    build!
  end
  def delegate(m)
    @binding.receiver.send(m,*@standard_args,**@kwargs,&@block)
  end
  private
    def build!
      set_block(&@binding.eval('Proc.new')) if @binding.eval('block_given?')
      @binding.eval('method(__method__)') 
        .parameters.each do |type,name|
          next if type == :block
          val = @binding.local_variable_get(name)
          if type =~ /key/
            @kwargs.merge!(type == :keyrest ? val : {name => val})
          else  
            type == :rest ? @standard_args.concat(val) : @standard_args << val
          end
        end
    end
    def set_block(&block)
      @block = block
    end
end

Usage:

class B 

  def some_method(a,b,c, *d, e:, f:, g: nil, **h)
    ArgsBuilder.new(binding).delegate(:some_other_method)      
  end

  def some_other_method(a,b,c, *d, e:, f:, g: , **h)
     yield __method__ if block_given?
    "I received: #{[a,b,c,d,e,f,g,h].inspect}"
  end
end

B.new.some_method(1,2,3,4,5,6, e: 7, f: 8, p: 9, n: 10) do |m| 
  puts "called from #{m}"
end
# called from some_other_method
#=> "I received: [1, 2, 3, [4, 5, 6], 7, 8, nil, {:p=>9, :n=>10}]"
#                 a  b  c  ----d----  e  f   g   -------h-------

Seems simple enough to be a helper and handles named and anonymous block passing across the methods as well.

TL;DR

This obviously requires matching or at least acceptable like signatures in the delegated method similar to the way super works. However we could take this quite a few steps further and create classes for the argument types [:req,:opt,:rest,:keyreq,:key,:keyrest,:block] place them into a collection and then interrogate the method being delegated to to determine the correct args to pass through; however I am not sure that this type of example would fit well in an SO post.

Additional note: because build! is called in initialize the local variable binding is static at the point of instantiation of the ArgsBuilder class. e.g.

 def foo(n) 
   builder = ArgsBuilder.new(binding) 
   n = 17
   builder.delegate(:bar)
 end 
 def bar(n) 
   n 
 end 

  foo(42) 
  #=> 42

However

 def foo(n) 
   n = 17
   ArgsBuilder.new(binding).delegate(:bar)
 end 

 foo(42) 
 #=> 17

That does not mean though that mutable objects cannot be changed

 def foo(n)
   builder = ArgsBuilder.new(binding) 
   n.upcase!
   builder.delegate(:bar)
 end

 foo('a')
 #=> "A"

You could obviously change this by just moving the call to build!

Upvotes: 2

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

Just out of curiosity, you might with a help of Module.prepend:

class Origin
  def m1(p, *args, **kw)
    m2()
  end

  def m2(p, *args, **kw)
    puts "p: #{p}, args: #{args.inspect}, kw: #{kw.inspect}"
  end
end

module Wrapper 
  def m1(*args, **kw)
    @__args__, @__kw__ = args, kw
    super
  end

  def m2(*)
    super(*@__args__, **@__kw__)
  end
end

Origin.prepend(Wrapper)

Origin.new.m1(:foo, 42, bar: :baz)
#⇒ p: foo, args: [42], kw: {:bar=>:baz}

Upvotes: 1

sawa
sawa

Reputation: 168081

Perhaps you mean this

def some_method *args, **kwargs, &block
  ...
  some_other_method(*args, **kwargs, &block)
  ...
end

But I think it is cleaner to use method delegation on some _other_method.

Upvotes: 1

Related Questions