Using Ruby’s Method Class for Fun and Profit
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 addMethod#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.