Ruby - Memoization
- 02 October 2016
- Ruby
- Ruby - Memoization
Hi. Today I would like to talk about memoization in Ruby.
But to begin, let's consider some abilities of Ruby. Often, we want to assign result of execution of function to some variable. If result of execution is false
or nil
we would like to assign some default value.
Let's show possible, but ugly code:
if @page.title
site_title = @page.title
else
site_title = "Blog about Ruby and Ruby On Rails"
end
In this case @page.title
can be nil
if user didn't fill in page title. If title of page is not nil
- we use it. If it's nil
- we return default value.
Now let's check how can we implement that logic without if..else
:
site_title = @page.title || "Blog about Ruby and Ruby On Rails"
If describe that code by words, that would be something like that: if @page.title
is not nil
or false
, assign it's value to site_title
, else use default value.
Let's remember that idea with ||
and move forward to memoization.
Here is how wiki describes memoization:
In computing, memoization is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.
Main idea to make sure that when function is being called it caches results and re-uses it on next calls, avoiding heavy calculations.
Let's consider code which isn't using memoization. In ApplicationController
we added method current_user
, which other controllers of our app use to get logged-in user:
class ApplicationController < ActionController::Base
def current_user
User.find(session[:user_id])
end
end
This method fetches data every time (!) when we call current_user
method, sending query to DB. If we add memoization we will fetch user data only once (on first call of current_user
).
Adding memoization is as simple as using ||
which we discussed at the beginning of this article:
class ApplicationController < ActionController::Base
def current_user
@current_user ||= User.find(session[:user_id])
end
end
Again, if we describe by words that code, we will get the following: if class variable @current_user
equals to false
or nil
- try to find user by session param and assign it to @current_user
. If user already fetched and variable @current_user
is set, just return that value.
So memoization in Ruby can be done as easy as using ||=
.
If you want to add memoization to method which is more than one line of code - it's ok, you can use begin..end
for that:
def roles_with_users_count
@roles ||= begin
Role.all.inject({}) do |hash, role|
hash.update(role.name => User.with_role(role).count)
end
end
end
Code inside begin..end
is not that important. The main part here is the idea of using begin..end
for memoization.
Use memoization: if you do heavy calculations, or fetching data from database and it's enough for you to do that once. It's just some sort of caching.
Don't use memoization in methods, which accepts params. In this case results of calculation will change depending of accepted param. But even for such cases there are options.
Don't use memoization in methods which operates by instance variables as well:
# don't do that
def full_name
@full_name ||= "#{@first_name} #{@last_name}"
end
@first_name = 'Sergii'
@last_name = 'M'
puts full_name # => "Sergii M"
@first_name = 'John'
@last_name = 'Doe'
puts full_name # => "Sergii M"
Variables changed, but we already cached initial value into @full_name
. And return it every time when we call full_name
without any connection to current state of @first_name
and @last_name
.
Good luck in using of memoization in your projects!
PS: if some examples are hard to understand, it's good idea to test everything in irb
on some simpler examples:
a = nil || "foo" # => "foo"
b = false || "bar" # => "bar"
c = "foo" || "bar" # => "foo"
Or this example:
def get_number
puts "getting number"
10
end
def memoized_number
@number ||= get_number
end
On first call of memoized_number
it calls get_number
and we will see "getting number" and 10 will be assigned to @number
. On second call of memoized_number
we will not see "getting number", because it will not call get_number
it will return cached value of 10.
Thanks for reading!