Hashrocket.com / blog

Large gold

Using Ruby's Method Class for Fun and Profit

posted on and written by in

Image 100x100 nick palaniuk

Lets try to use some of the methods in Class: Method

First some setup, we will create a Foo module with a bar method and include it in the String class.

module Foo
  def bar(x)
    self << "#{x}"
  end
end
String.send(:include, Foo)

We will start with a String instance.

pry(main)> str = "foo"
=> "foo"

We can create an instance of the Method class like so:

pry(main)> bar_meth = str.method(:bar)
=> #<Method: String(Foo)#bar>

If we wanted to we could invoke this with Method#call, but we are more interested in the Method instance.

pry(main)> bar_meth.call("baz")
=> "foobaz"

We often want to know where a method is defined. We can use Method#source_location to find out. I loaded the initial Foo module into the REPL in a file called 'wut.rb'

pry(main)> bar_meth.source_location
=> ["wut.rb", 2]

If the method is defined in the core library Method#source_location will return nil.

pry(main)> str.method(:length).source_location
=> nil

We could wrap this in a predicate method called core? and add to Method

module MethodHelpers
  def core?
    self.source_location.nil?
  end
end
Method.send(:include, MethodHelpers)

pry(main)> bar_meth.core?
=> false

We can check where the method of interest is scoped with Method#owner

pry(main)> bar_meth.owner
=> Foo

This means we can check the instance methods en masse for any modules or external libraries in our inheritance chain. Here I will create a Hash with the owners as keys and the values an array of their methods.

pry(main)> str.methods.each_with_object(Hash.new {|h, k| h[k] = []}) do |m, h| 
  h[str.method(m).owner] << m
end
=> {String=>
  [:<=>,
   :==,
   :===,
   :eql?,
   :hash,
   :casecmp,
   ...
   Foo=>[:bar]
   ...
  ]}

We can add a little more information to this if we throw in the Method#arity and Method#parameters methods in there as well.

pry(main)> str.methods.each_with_object(Hash.new {|h, k| h[k] = []}) do |m, h| 
  h[str.method(m).owner] << [m, str.method(m).arity, str.method(m).parameters] 
end

=> {String=>
  [[:<=>, 1, [[:req]]],
   [:==, 1, [[:req]]],
   [:===, 1, [[:req]]],
   [:eql?, 1, [[:req]]],
   [:hash, 0, []],
   [:casecmp, 1, [[:req]]],
   ...
   Foo=>[[:bar, 1, [[:req, :x]]]],
   ...
  ]}

Now let's create a new Object#methods. We will call this #method_list and I'll put it on object as well. It will look similar to the previous hash. I'm going to add Method#source_location to the values array and a parameter for passing a boolean if you want to exclude core methods from the list. We can also significantly clean it up by iterating over an array of Method objects instead of the method name.

module MethodList
  def method_list(core = true)
    total_meth    = self.methods.map {|m| self.method(m) }
    non_core_meth = self.methods.map {|m| self.method(m) }.select { |m| !m.core? }

    list = core ? total_meth : non_core_meth

    list.each_with_object(Hash.new {|h, k| h[k] = []}) do |m, h|
      h[m.owner] << [m.name, m.arity, m.parameters, m.source_location]
    end
  end
end
Object.send(:include, MethodList)

pry(main)> str.method_list(false)
=>...
Foo=>[[:bar, 1, [[:req, :x]], ["wut.rb", 2]]],
...

And there we have it - usefulness debatable. I find Method#source_location invaluable for looking up the source in gems, but I would be curious to hear how other people are using any of the other methods in the Method class.

Posted in Ruby