Why defining methods in Ruby returns the method name
Have you ever tried defining a method in irb? If so you might have noticed that doing so returns the name of the method:
$ irb
>> def foo
>> end
=> :foo
That turns out to be useful. Allow me to paint you a picture:
Imagine that you have a class with a method you would like to benchmark
class SomeClass
def some_method
puts "running code"
end
end
For this simple example it obviously wouldn’t be interesting to benchmark #some_method
but imagine this method did lots of complicated and possibly slow things.
A quick way to add benchmarking to this class could be
require "benchmark"
class SomeClass
def some_method
benchmark(:some_method) do
puts "running code"
end
end
private
def benchmark(method)
return_value = nil
time = Benchmark.realtime { return_value = yield }
ms = (time * 1_000).round(10)
puts "#{method} took #{ms} ms"
return_value
end
end
Here we add a #benchmark
method that takes the name of the method we’re benchmarking and a block. It will then run the block and measure how long time it takes. The time is printed to the screen and the return value of the block is returned.
Benchmarking methods in this way is probably something we’ll want to do a lot, so lets pull it out into a reusable module
module Benchmarking
def benchmark(method)
return_value = nil
time = Benchmark.realtime { return_value = yield }
ms = (time * 1_000).round(10)
puts "#{method} took #{ms} ms"
return_value
end
end
class SomeClass
include Benchmarking
def some_method
benchmark(:some_method) do
puts "running code"
end
end
end
This is better, but having benchmark do ... end
in all your methods is a bit noisy. Something like this would be nicer
def some_method
puts "running code"
end
benchmark :some_method
This is similar to the helper_method
method found in Rails controllers.
The code to accomplish that is
require "active_support/concern"
module Benchmarking
extend ActiveSupport::Concern
module ClassMethods
def benchmark(method)
aliased_name = "__unbenched_#{method}__"
alias_method aliased_name, method
define_method(method) do |*args|
benchmark(method) { send(aliased_name, *args) }
end
method
end
end
def benchmark(method)
return_value = nil
time = Benchmark.realtime { return_value = yield }
ms = (time * 1_000).round(10)
puts "#{method} took #{ms} ms"
return_value
end
end
class SomeClass
include Benchmarking
def some_method
puts "running code"
end
benchmark :some_method
end
Our approach here is to redefine #some_method
to do what it did originally, but wrap the body in benchmark(:some_method) do ... end
. This is done by aliasing #some_method
to a new method called #__unbenched_some_method__
. This effectively creates a new method with this new funky name, but with the old body.
We then define a new method with the original name that calls our aliased method while wrapping that call in benchmark
.
You can imagine the resulting code looking like this
class SomeClass
include Benchmarking
def some_method(*args)
benchmark(:some_method) do
send(:__unbenched_some_method__, *args)
end
end
def __unbenched_some_method__
puts "running code"
end
end
The reason we need ActiveSupport::Concern
is because that is the easiest way to add both class and instance methods with a single module.
This works well but it has one issue: We have to type some_method
twice
def some_method
puts "running code"
end
benchmark :some_method
We can fix this by abusing the fact that def some_method ... end
returns :some_method
. That means we can actually just write
benchmark def some_method
puts "running code"
end
Our end result it
require "benchmark"
require "active_support/concern"
module Benchmarking
extend ActiveSupport::Concern
module ClassMethods
def benchmark(method)
aliased_name = "__unbenched_#{method}__"
alias_method aliased_name, method
define_method(method) do |*args|
benchmark(method) { send(aliased_name, *args) }
end
method
end
end
def benchmark(method)
return_value = nil
time = Benchmark.realtime { return_value = yield }
ms = (time * 1_000).round(10)
puts "#{method} took #{ms} ms"
return_value
end
end
class SomeClass
include Benchmarking
benchmark def some_method
puts "running code"
end
end
While this will probably look foreign to most Rubyists I would argue it is quite an elegant way to decorate methods.
A real world use case for this taken from Tonsser’s API is memoizing expensive methods. We have lots of code like this
class SomeClass
include Memoize
memoize def slow_method
puts "running slow code"
end
end
This is only possible because defining a method returns the name of that method.