DEV Community

Ritikesh
Ritikesh

Posted on • Edited on

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 
}

Top comments (2)

Collapse
 
rhymes profile image
rhymes

One detail: if you store the result of the User.all in an instance variable in the controller and access it from the template, Rails does not go back to the DB twice, so you might not need memoization after all.

In addition, ActiveRecord caches results by itself in some cases: guides.rubyonrails.org/association...

Collapse
 
ritikesh profile image
Ritikesh

You are right, ActiveRecord does cache results, but not always. Like when you use scopes or self methods or finder methods etc. In most real-world use cases, you wouldn't do a User.all anyway. I'll try to update to a better example sometime later, but the gist was to suggest how to memoize dynamic methods. :)