Ruby
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 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.