Amit Liber
Amit Liber

Reputation: 51

how to send dynamic arguments to methods with default args

I am trying to figure if there's a way of using send for calling a method with dynamic arguments (*args, **kwrags) while the method has only default arguments.

Given the following class:

class Worker
  def perform(foo='something')
    # do something
  end
end

I have a module which needs to check some conditions. If the conditions are not met, the #perform method should run:

module SkippableWorker
  def self.included(base)
    base.class_eval do
      alias_method :skippable_perform, :perform
      define_method :perform do |*args, **keyword_args|
        return if skip?
        self.send(:skippable_perform, *args, **keyword_args)
      end

      define_method :skip? do
        # checks whether to skip or not
      end
    end
  end
end

Once including the module in the Worker class, I encounter a problem in the module's send command:

self.send(:skippable_perform, *args, **keyword_args)

If once wants to call #perform and use the default arguments, the call will be made without any arguments at all:

Worker.new.perform()

But the send command in the module translates the *args and **kwargs to [] and {}. Thus, the call to #perform ends up to be send(:perform, [], {}) which causes ArgumentError for args amount mismatch or for the very least, sends a value one did not mean to send ([] instead of the default 'something').

Any ideas how to overcome this?

Upvotes: 3

Views: 3034

Answers (1)

Eric Duminil
Eric Duminil

Reputation: 54233

Problem

The best explanation I found was here.

**{} is not parsed in the same way as **kwargs, even if kwargs is an empty hash.

keyword_args.empty?

One simple solution would be to check if kwargs is empty before calling skippable_perform.

The complete code becomes :

module SkippableWorker
  def self.included(base)
    base.class_eval do
      alias_method :skippable_perform, :perform
      define_method :perform do |*args, **keyword_args|
        return if skip?
        if keyword_args.empty?
          self.send(:skippable_perform, *args)
        else
          self.send(:skippable_perform, *args, **keyword_args)
        end
      end

      define_method :skip? do
        puts self.class
        if rand > 0.5
          puts "  Skipping"
          true
        end
      end
    end
  end
end

class Worker
  def perform(foo='standard arg')
    puts "  #{foo}"
  end
  include SkippableWorker 
end

class AnotherWorker
  def perform(bar:'standard kwarg')
    puts "  #{bar}"
  end
  include SkippableWorker 
end

Worker.new.perform
Worker.new.perform
Worker.new.perform('another arg')
Worker.new.perform('another arg')
AnotherWorker.new.perform
AnotherWorker.new.perform
AnotherWorker.new.perform(bar: 'another kwarg')
AnotherWorker.new.perform(bar: 'another kwarg')

Depending on rand, it could output :

Worker
  standard arg
Worker
  Skipping
Worker
  Skipping
Worker
  another arg
AnotherWorker
  standard kwarg
AnotherWorker
  standard kwarg
AnotherWorker
  Skipping
AnotherWorker
  another kwarg

Method#parameters

Another method would be to check which parameters are expected by skippable_perform :

method(:skippable_perform).parameters.any?{|type,name| type == :key}

and adapt define_method depending on the result.

Upvotes: 3

Related Questions