Ruby Memoization using Singleton Method

There is a saying in Rails community:

“ A good rails developer may not be a good ruby developer but a good ruby developer is always a good rails developer.”

But this fact is often neglected and people starts working on rails without fully understanding core concepts of ruby. So, I come up with a simple but powerful core ruby concept that can be used in rails or other ruby based projects to spread the meaning of above proverb and to show the power of Ruby.

You might have often seen this kind of code pattern in rails projects:

class User
def format_name
do_something_with name
do_another_thing_with name
end
  def name
@name||= begin
User.first.name
end
end
end

In above code name method is called multiple times and that’s why we are using memoization to prevent multiple database calls.

There is one more way to do memoization that is using concept of singleton method.

class User
def format_name
do_something_with name
do_another_thing_with name
end
  def name
def self.name
@name
end
    @name = User.first.name
end
end

Following are the concepts behind the above code:

1. Whenever you are going to call name method for first time, It is going to create Singleton method - name for declared object.

2. Whenever you will call Singleton method for 2nd time, it is going to execute Singleton - name method instead of main class name method because Singleton method have high preference over main class methods.

3. Singleton method have access to all instance variables of an object.

Above methodology can be used when code inside begin block is complex, or large to simplify it.

This way also memoizes `false` and `nil` values as well:

For example:

class Foo < Object
def thing1
do_something with: expensive_computation1
do_something_else with: expensive_computation1
end
  def thing2
do_something with: expensive_computation2
do_something_else with: expensive_computation2
end

private
  def expensive_computation1
@expensive_computation1 ||= Model.where(id: 1742).first
end
  def expensive_computation2
def self.expensive_computation2
@expensice_computation2
end

@expensive_computation2= Model.where(id: 4217).first
end
end

thing1 and thing2 both are memoized but thing2 also memoizes when the record is not found. thing1 will go hit the db again.

Benchmark comparison of different memoization methods:

require "benchmark"
class A
def name
@name ||= begin
rand
end
end
end
class B
def name
return(@name) if defined?(@name)
    @name = rand
end
end
class C
def name
def self.name
@name
end
    @name = rand
end
end
class D
def name
class << self
def name
@name
end
end
    @name = rand
end
end
n = 20_000
n1 = 2_000
Benchmark.bm(2) do |x|
x.report("A:") { n.times { k = A.new; n1.times { k.name } } }
x.report("B:") { n.times { k = B.new; n1.times { k.name } } }
x.report("C:") { n.times { k = C.new; n1.times { k.name } } }
x.report("D:") { n.times { k = D.new; n1.times { k.name } } }
end

Output:

user     system      total        real
A: 3.810000 0.000000 3.810000 ( 3.817210)
B: 4.000000 0.010000 4.010000 ( 4.007852)
C: 2.850000 0.010000 2.860000 ( 2.848843)
D: 2.850000 0.000000 2.850000 ( 2.854403)

Although there is not much difference in time but comparatively ‘C’ is faster.

Also it’s worth noting that initialisation of singleton method is slower than Boolean initialisation but calling singleton method is much faster.

I hope you would like this new way of memoization. Suggestions to improve the code and article are most welcome. :-)