What's New in Edge Rails: Easy Memoization 27

Posted by ryan
at 8:32 PM on Tuesday, July 15, 2008



Most people will recognize the pattern of memoization to provide a basic caching mechanism (that’s not a misspelling, it really doesn’t have an ‘r’) :

1
2
3
4
5
6
class Person < ActiveRecord::Base
  def social_security
    @social_security ||= decrypt_social_security
  end
  ...
end

The big problem with this common type of memoization is that you’ve littered your method implementation with caching logic. Caching is best applied in a transparent manner – and ActiveSupport now lets you easily insert memoization into your classes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person < ActiveRecord::Base

  def social_security
    decrypt_social_security
  end

  # Memoize the result of the social_security method after
  # its first evaluation (must be placed after the target
  # method definition).
  #
  # Can pass in multiple symbols:
  #  memoize :social_security, :credit_card
  memoize :social_security
  ...
end

@person = Person.new
@person.social_security  # decrypt_social_security is invoked
@person.social_security  # decrypt_social_security is NOT invoked

memoize transparently aliases the method and stores the value of your method’s first evaluation in an instance variable – giving you the same functionality of the unrefined var ||= ... implementation with much less clutter.

This implementation also handles the case where you freeze your User object before ever calling the social_security method. Using conventional memoization, calls to social_security would give you a Can’t modify instance variable error instead of happily evaluating as you would want.

This implementation also will not execute the target method more than once if the original execution resulted in nil or false – which is a flaw of the conventional pattern. You can, however, force the target method to be invoked with the optional reload parameter:

1
2
3
# Force invocation of target method, i.e. "decrypt_social_security"
# is invoked independent of there being a cached value
@person.social_security(true)

So start giving memoize some play – it’s just the right thing to do.

Update: You can now use unmemoize_all and memoize_all to undo and redo your memoized properties.

tags: ruby, rubyonrails

Comments

Leave a response

  1. Michael BleighJuly 15, 2008 @ 09:48 PM

    Hmm, I’m not sure I’m 100% a fan of this implementation. As it stands this is more code for less clarity (a separate memorize call that could get lost in the model if separated from the method definition). Perhaps I’m not seeing the big picture?

  2. Anthony MillsJuly 15, 2008 @ 10:00 PM

    So the “memorize” call—is it misspelled?

  3. Andrew ZielinskiJuly 15, 2008 @ 10:56 PM

    I agree with Michael, I don’t really like this either. If you need to memoize an instance variable which isn’t the value returned form the method then would you revert to the ‘old way’ of doing things? I don’t know but checking to see whether a variable has been set or not and to initialize it if the case is latter just seems more logical to occur in code.

    btw I think the ruby way of doing this is ‘very clean’

  4. Matt IttigsonJuly 15, 2008 @ 11:07 PM

    Is this the pre-cursor to a plugin or an easy extension point which let’s you memoize to something like memcache instead of the class itself? Moving this pattern out of the methods and into the meta arena certainly makes it easier to implement a system-wide memoization strategy.

  5. Bronson McKinleyJuly 15, 2008 @ 11:35 PM

    Very cool. A simple, great idea. I have a few spots where this will make my code more readable and concise. I like to look at my methods and see what they are doing. Its a great idea to take any caching logic out of them since its generally there for performance reasons and not functionality.

    @Michael well its less code if you consider typing out “if instance_variable_defined?( :@v ) @v else @v = ... end” compared to “memorize”. ”||=” is ok sometimes but doesnt work so well ( at all ) if there is a nil or false value returned. But I think the benefit is that your method just reads as if performance was not an issue. And when you start optimizing stuff you can cache values without mucking around in your perfectly readable methods. You just slap a memorize declaration in there and your set.

    I dunno… I like it.

  6. Joshua PeekJuly 16, 2008 @ 01:15 AM

    memorize => memoize

    It was a type and I fixed in the next commit (http://github.com/rails/rails/commit/001c8beb4d0999a858a8b52ad511ee1251cc3517)

    The reason reason for the abstraction was to fix freeze. If you freeze an object with memoized methods that haven’t been eager loaded, you get “Can’t modify instance variable error”. This fixes that little pain point as well.

  7. leethalJuly 16, 2008 @ 02:28 AM

    What about stuff thar returns nil? Like, @current_user ||= User.find_by_id(session[:user]). That’ll run every time you call the current_user method if the find_by_id returns nil. Does memoize fix zhat?

  8. Roman Le NégrateJuly 16, 2008 @ 04:33 AM

    Shouldn’t the line “memoize :social_security” be placed below the definition of #social_security?

  9. BrianJuly 16, 2008 @ 05:11 AM

    I think memoizing is cool. I note however that this implementation of memoize does not work if the method the user needs to memoize takes parameters. I believe such capability is (surprisingly) easily added though—see the several lines of code of the memoize gem that does just that, for example).

  10. RyanJuly 16, 2008 @ 07:30 AM

    Josh – thanks for your comments. I’ve updated the post with your clarifications.

    Roman – you’re right about the memoize call needing to be below the memoized target.

    lethal – this memoize does NOT call the original method more than one once, even if the original evaluation returns nil. It keys its execution based on the mere existence of the cached instance variable and not its value.

  11. Ahmed SobhiJuly 16, 2008 @ 08:17 AM

    Glad to see this feature make it into rails. I’ve implemented it previously as a plugin called method_cache(http://github.com/humanzz/method_cache, http://humanzz.spaces.live.com/blog/cns713.entry ). It just had one more feature: in addition to caching the result in instance variable, it also caches the result in the Rails cache store. I think that would make a nice addition to the memoize feature

  12. ChrisJuly 16, 2008 @ 09:15 AM

    I’ve never been fully understanding of the implications of using ||=. Is there a tutorial or blog post that looks at using this in depth in a rails environment?

  13. OlegJuly 16, 2008 @ 10:18 AM

    @Chris

    ||= caching is a pretty simple concept. Let assume we have something like this:

    class Foo def uncached_users User.find(...some crazy complex sql…) end end

    def cached_users
      @users ||= User.find(...some crazy complex sql...)
    end

    So within execution cycle uncached_users method it will hit the database every single time you call it. On the other hand call to cached_users will populate instance variable that will get re-used in consequent calls instead of hitting database, thus it’s faster.

  14. OlegJuly 16, 2008 @ 10:25 AM

    Blog killed the formatting, sorry.

    Also what’s up with :memoise? It’s not even a word. Is it some type of “plane’arium” type of a joke?

  15. ChrisJuly 16, 2008 @ 10:31 AM

    @oleg: Thanks, therein lies my confusion, what defines an execution cycle.

    So in your example a call to a controller/action is made, the controller does @users ||= and lets say the view just prints that out in a list.

    Now lets say the user hits refresh and it goes to the same controller/action, except this time @user is already set from the last page request and thus won’t execute the right hand side of the ||=, is that right? Does that apply to just the session making the call or to all sessions?

  16. OlegJuly 16, 2008 @ 10:43 AM

    @chris: It applies to the current request only.

  17. ChrisJuly 16, 2008 @ 10:59 AM

    @oleg: That is I thought. I find it confusing when I see people do a decleration like:

    @users ||= Users.all

    When its only set in that call and then the view just prints it out. Since it isn’t set anywhere else and just used in the view then why bother using ||= at all. Does it help if you do that and then access associations such as @user.company.name?

    I see usage of ||= in Railscasts by RBates when it seems like he is only setting the var nad not really accessing it again except in a view and it didn’t make sense to me.

    Thanks for help!

  18. Jonathan BarrettJuly 16, 2008 @ 03:04 PM

    @chris: Let’s say you’re in a blog and are displaying a category of blog posts, like:

    @category.some_obscure_posts

    where some_obscure_posts is a method doing some crazy database hitting and processing. If I want to show:

    <= @category.some_obscure_posts.size ->

    At the top of the page, then

    <% @category.some_obscure_posts.each do |post| -%>

    further down, that’s twice I’m hitting that database with the crazy request, all in the same page load. THAT’S what this mitigates.

  19. J. Ryan SobolJuly 16, 2008 @ 03:06 PM

    Please tell me this is going into rails as a plugin, or probably better, as a module that can be extended like Forwardable?

    This really has no business being in rails core, IMO.

  20. Andrew ZielinskiJuly 16, 2008 @ 05:06 PM

    WOW, I like the fact that it doesn’t execute the original method even if it returned nil or false.

    Just wondering could you do this:

    class Person < ActiveRecord::Base end

    def social_security
      @social_security = decrypt_social_security
    end
    memoize :social_security
    ...

    I would like the code to explicitly indicate that you are assigning a value to an instance variable which is lost in the posts approach.

  21. Andrew ZielinskiJuly 16, 2008 @ 05:13 PM

    hmm.. or was the instance variable added purely to accommodate caching?

  22. Christopher BottaroJuly 16, 2008 @ 08:02 PM

    What about when you want to force a reload? Usually my memoized methods look something like this…

    def f(reload = false)
      @f = do_f if @f.blank? or reload
      @f
    end
  23. RyanJuly 18, 2008 @ 01:00 PM

    Christopher, your reload request has been answered. I’ll update the post shortly.

  24. FinanzamtJuly 19, 2008 @ 03:35 PM

    @Ryan – very usefull post!

  25. Jocke SelinJuly 22, 2008 @ 04:51 AM

    Whilst I find this a bi pointless, I can’t argue against the fact that a) it does have its place, b) it is a reconized technique: http://www.reference.com/browse/wiki/Memoization

    But, I think it’s abstracted from the programmer in a way that means that you’ll have to go look for the call to memoize which can lead to a lot of confusion. I’d like to see this in either a block, or using some sort of altered name.

    Something like: def social_security Person.memoize do decrypt_social_security end end

    Or perhaps: def social_security memoize(decrypt_social_security) end

    Or: def social_security memoize_decrypt_social_security end

    This is way beyond my Ruby skillz, so be gentle with me. My point is, the implementation from a programmers point of view is abstract. It’s not easy to spot if somethings memoised or not. :)

  26. mormonAugust 07, 2008 @ 09:42 AM

    I like it! Though it seems a little halting to have to define it twice…I almost wonder if ||= isn’t about as good.

  27. Igor SuttonAugust 18, 2008 @ 12:05 PM

    Perhaps the only wrong thing in fact was the example. The memoization technique is often used to cache responses of expensive computations. Think about the factorial example in here:

    http://www.reference.com/browse/wiki/Memoization

    I used it a lot to cache DNS responses during slow time, or even to cache database connections easily. I wouldn’t do that like the given example, but it is a really nice and clean technique, indeed.

Comment