Ruby/Rails : Memoization with dynamically defined methods

ritikesh

Ritikesh

Posted on February 7, 2019

Ruby/Rails : Memoization with dynamically defined methods

Ruby(and Rails for that matter) is pretty magical and I am sure most people working on it or those who have written any sort of code in the language at all will agree with me on this.

Mind=Blown!

One of the most common tricks that Ruby allows you to do, and is used almost everywhere in Rails as well, is the ability to dynamically define methods. You can do so by simply, you guessed it right, calling define_method inside a class.

class User < ActiveRecord::Base

    scope :active, -> { where(active: true) }
    scope :visible, -> { where(visible: true) }
    scope :deleted, -> { where(deleted: true) }

    FILTERS = %w[active visible deleted]

    FILTERS.each { |filter|
        define_method filter do
            self.class.send(filter)
        end 
    }
end

Now that we have seen what define_method can do, let's move on to some jargon. Memoization in general means to cache complex computation or network calls in-memory. Memoization is a fairly common practise in most real-world applications and a simple example of how it is used in rails in daily life:

    def get_users
        @users = User.where(conditions...).all
    end

    def get_users_memoized
        @users ||= User.where(conditions...).all
    end

In an ideal production app, you would use pagination as well, but I would like to keep things simple for now. Say if you called the method get_users multiple times as part of rendering a page, it would make a query to the database and fetch the list of users every time you call the method, whereas upon calling memoized_results multiple times, only the first time the database would be hit and the results of the query would be cached in the instance variable @users.

Coming back to the dynamic_method definition earlier, you would need a hack to apply the same memoization technique here as you cannot use a single static variable for memoizing multiple different methods. Snippet below demonstrates on how we can use meta-programming(more ruby magic ;)) to overcome this limitation:

# memoize db/network responses in instance variable dynamically
def memoize_results(key)
  return instance_variable_get(key) if instance_variable_defined?(key)
  instance_variable_set key, yield  
end

# usage
FILTERS.each { |filter|
    define_method filter do
        memoize_results("@#{filter}_users") do
            self.class.send(filter)
        end
    end 
}
💖 💪 🙅 🚩
ritikesh
Ritikesh

Posted on February 7, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related