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.

This post is written by David Pedersen
David is Backend Engineer at Tonsser
On Twitter On GitHub

Like what you're reading? Then subscribe to our newsletter
No spam, just one email every two weeks with the posts we published in that time