In Ruby, should I use ||= or if defined? for memoization?
Be careful: x ||= y assigns x = y if x returns false. That may mean that x is undefined, nil, or false.
There are many times variables will be defined and false, though perhaps not in the context of the @current_user_session instance variable.
If you desire conciseness, try the conditional construct:
defined?(@current_user_session) ?
@current_user_session : @current_user_session = UserSession.find
or just:
defined?(@current_user_session) || @current_user_session = UserSession.find
if you just need to initialize the variable.
When to use memoization in Ruby on Rails
I think many Rails developers don't fully understand what memoization does and how it works. I've seen it applied to methods that return lazy loaded collections (like a Sequel dataset), or applied to methods that take no arguments but calculate something based on instance variables. In the first case the memoization is nothing but overhead, and in the second it's a source of nasty and hard to track down bugs.
I would not apply memoization if
- the returned value is merely slightly expensive to calculate. It would have to be very expensive, and not further optimizable, for it to be worth memoization.
- the returned value is or could be lazy loaded
- the method is not a pure function, i.e. it is guaranteed to return exactly the same value for the same arguments -- and only uses the arguments to do it's work, or other pure functions. Using instance variables or calling methods that in turn uses instance variables means that the method could return different results for the same arguments.
There are other situations too where memoization isn't appropriate, such as the one in the question and the answers above, but these are three that I think aren't as obvious.
The last item is probably the most important: memoization caches a result based on the arguments to the method, if the method looks like this it cannot be memoized:
def unmemoizable1(name)
"%s was here %s" % name, Time.now.strftime('%Y-%m-%d')
end
def unmemoizable2
find_by_shoe_size(@size)
end
Both can, however, be rewritten to take advantage of memoization (although in these two cases it should obviously not be done for other reasons):
def unmemoizable1(name)
memoizable1(name, Time.now.strftime('%Y-%m-%d'))
end
def memoizable1(name, time)
"#{name} was here #{time}"
end
memoize :memoizable1
def unmemoizable2
memoizable2(@size)
end
def memoizable2(size)
find_by_shoe_size(size)
end
memoize :memoizable2
(assuming that find_by_shoe_size
didn't have, or relied on, any side effects)
The trick is to extract a pure function from the method and apply memoization to that instead.
What is the reason to use ||= instead of = in model to define a constant
||=
is used as a cheap way to memoize the value, as the other posters mention. However...
Why memoize a constant?
The author is most likely protecting against warnings when loading that source file multiple times. (warning: already initialized constant VALID_VIDEO_HOSTS
)
ruby memoization, efficiency
Your example
@value ||= calculate_value
is equivalent to
@value || @value = calculate_value
and is not equivalent to
@value = @value || calculate_value
Therefore the answer is: It is very efficient. It is not re-assigned each time.
What are the drawbacks of using ||= syntax to perform memoization
Well, the biggest reason I'm aware of is that you can't memoize nil
or false
values like that.
Is there a convention for memoization in a method call?
I would do it like this:
def filesize
@filesize ||= calculate_filesize
end
private
def calculate_filesize
# ...
end
So I'd just name the method differently, as I think it makes more sense.
How can I memoize a method that may return true, false, or nil in Ruby?
Explicitly check if the value of @x_query
is nil
instead:
def x?
@x_query = expensive_way_to_calculate_x if @x_query.nil?
@x_query
end
Note that if this wasn't an instance variable, you would have to check if it was defined also/instead, since all instance variables default to nil
.
Given your update that @x_query
's memoized value can be nil
, you can use defined?
instead to get around the fact that all instance variables default to nil
:
def x?
defined?(@x_query) or @x_query = expensive_way_to_calculate_x
@x_query
end
Note that doing something like a = 42 unless defined?(a)
won't work as expected since once the parser hits a =
, a
is defined before it reaches the conditional. However, this isn't true with instance variables since they default to nil
the parser doesn't define them when it hits =
. Regardless, I think it's a good idiom to use or
or unless
's long block form instead of a one-line unless
with defined?
to keep it consistent.
Define all or most of the functions in a class with memoization by name
I'd recommend implementing this with the memoist ruby gem:
require 'memoist'
class MyClass
extend Memoist
def one_good_name
# some good logic ...
end
memoize :one_good_name
end
Memoizing functions with multiple parameters in Ruby
When you do not need to print Cache hit!
than I would do something like this:
def area(length, width)
@cache ||= {}
@cache["#{length},#{width}"] ||= length * width
end
Or if you need some output, but a Cache miss!
is fine too:
def area(length, width)
@cache ||= {}
@cache.fetch("#{length},#{width}") do |key|
puts 'Cache miss!'
@cache[key] = length * width
end
end
If you want to accept even more arguments you might want to use something like this:
def area(*args)
@cache ||= {}
@cache[args] ||= args.inject(:*)
end
Related Topics
How to Change the Zone Offset for a Time in Ruby on Rails
Why Relative Path Doesn't Work in Ruby Require
Nameerror: Uninitialized Constant Faker
How to Append Data to JSON in Ruby/Rails
How to Get a List of All Available Rake Tasks in a Namespace
Passing @Object into a Rails Partial Render
How to Do Attr_Accessor_With_Default in Ruby
Time.Use_Zone Is Not Working as Expected
Synchronized Method for Concurrency in Ruby
How Are Array.Each and Array.Map Different
Is It Acceptable Practice to Patch Ruby's Base Classes, Such as Fixnum
Make: /Usr/Bin/Mkdir: Command Not Found During 'Gem Install Nokogiri' in Ubuntu 20.04
What Are the Conventional Gem Paths for Ruby Under Os X 10.5