Search code examples
rubytestingsymbolsprocdispatch

Calling private methods by symbol name in Ruby


I have the symbol name of a method that I'd like to call with some arguments. What I'm really trying to do boils down to this code snippet:

method.to_proc.call(method)

In this case, method is the symbol name of a method on the object. In my case, I'm trying to call a method that happens to be private on the object.

This is the error output that I get:

>$ ruby symbol_methods.rb 
symbol_methods.rb:33:in `call': private method `test_value_1' called for "value":String (NoMethodError)
    from symbol_methods.rb:33:in `block (2 levels) in <main>'
    from symbol_methods.rb:30:in `each'
    from symbol_methods.rb:30:in `block in <main>'
    from symbol_methods.rb:29:in `each'
    from symbol_methods.rb:29:in `<main>'

Here's a self-contained example that demonstrates this behavior:

data = [
  ["value", true],
  ["any value here", true],
  ["Value", true],
]

def matches_value(string)
  string == "value"
end

def contains_value(string)
  string.gsub(/.*?value.*?/, "\\1")
end

def matches_value_ignore_case(string)
  string.downcase == "value"
end

#tests
[:matches_value, :contains_value, :matches_value_ignore_case].each_with_index do |method, index|
  test = data[index]
  value = test[0]
  expected_result = test[1]
  result = method.to_proc.call(value)  # <<== HERE
  puts "#{method}: #{result == expected_result ? 'Pass' : 'Fail'}: '#{value}'"
end

The important bit is in the block marked #tests. The data variable is a set of inputs and expected results. The test_value_* methods are private methods that are the tests to run.

I've tried public_send(method, value) and method.to_proc.call(value), but both result in the private method error.

What would be the right way to call a private method named as a symbol in this case? I'm looking for both an explanation and a syntactically correct answer.


Solution

  • After a fair amount of searching, I found an alternative answer than Object#send, that has an unanticipated feature benefit. The solution is to use the Object#method to return a Method object for the symbol name.

    A Method object is a Proc-like callable object, so it implements the #call interface, which fits the bill nicely. Object has many such useful helpers defined in its interface.

    In context of the original question, this is how it works:

    #tests
    [:test_value_1, :test_value_2, :test_value_3].each do |method|
      data.each do |test|
        value = test[0]
        expected_result = test[1]
        puts "#{method}: #{self.method(method).call(value) == expected_result ? 'Pass' : 'Fail'}: '#{value}'"
      end
    end
    

    The important bits are:

    self.method(method).call(value)
    

    This will convert the symbol name to a Method object, and then invoke the method with value supplied as the parameter. This works roughly equivalently to the send method solution, in functional terms. However, there are some differences to note.

    send is going to be somewhat more efficient, as there's no overhead in the conversion to a Method. Method#call and send use different internal calling mechanisms, and it appears that send has less call overhead, as well.

    The unanticipated feature of using Object#method is that the Method object is easily converted to a Proc object (using Method#to_proc). As such, it can be stored and passed as a first-class object. This means that it can be supplied in place of a block or provided as a callback, making it useful for implementing flexible dispatch solutions.