Ruby Speedup: Memoize those Methods

My company, adPickles, needs to scale big. According to our estimates the site will have to serve pages into the series of tubes faster than the trucks will be able to carry them. Yeah. Something like 3-17 trucks per tube*. That fast.

So we’ve been keeping an eye on various types of caching. One of my favorites is simple method memoization of the kind we’ve all seen before:

    def last_user
      @last_user ||= User.something_Im_too_lazy_to_make_up
    end

Which is great. As long as you don’t need that value to be updated for the life of the object it’s in that’s a solid way to prevent the value from being calculated twice.

But it can get pretty ugly when you have a multi-line method:

    def current_advertising_balance
      return @current_advertising_balance if @current_advertising_balance
      amount_owed = Invoice.procces.something(:complicated) + OtherThing
      amount_paid = Payment.procces.something(:complicated) + OtherThing
      @current_advertising_balance ||= amount_owed - amount_paid
    end

It gets the job done just fine but it’s a hack. You’re using @current_advertising_balance in three (3!) places in the same method.

I just came across this easier way. I don’t know where I found this but I haven’t been using it until lately and I’m really starting to like it. Check this out:

Update: Thanks to Mourad for pointing out the missing ‘||=’

    def current_advertising_balance
      @current_advertising_balance ||= begin
        amount_owed = Invoice.procces.something(:complicated) + OtherThing
        amount_paid = Payment.procces.something(:complicated) + OtherThing
        amount_owed - amount_paid
      end
    end

Whoah. I know. But begin…end is always used to catch errors, right? Well, apparently it’s also a great way to encapsulate any block of code. It has virtually no overhead, doesn’t pollute the namespace, and is easy to read. And you get your error handling for free:

    def current_advertising_balance
      @current_advertising_balance ||= begin
        amount_owed = Invoice.procces.something(:complicated) + OtherThing
        amount_paid = Payment.procces.something(:complicated) + OtherThing
        amount_owed - amount_paid
      rescue
        0.0
      ensure
        Advertiser.mark_that_we_calculated_balance
      end
    end

* 1 truck per tube is equivalent to 1K requests per second

  • Ted said: When I first read that, I thought to myself, "but he's not caching anymore; he's recalculating every time the variable is referenced!" But I cracked open irb and tried it myself and sure enough it only evaluates once. Pretty slick. teflon-ted:~/Desktop/adPickles/rails ted$ irb >> a = 2 => 2 >> b = 3 => 3 >> m = begin a + b end => 5 >> a = 7 => 7 >> m => 5 Of course that means your "current_advertising_balance" is only "current" from the time that it was first evaluated ;-)
  • Ted said: Apologies for the poorly formatted prior post - gawd I hate forms without preview buttons :-(
  • Mourad said: Hi I think you have typo in your last examples It should be :
    @current_advertising_balance ||= begin
    ...  
    end
    
    For Ted I your example the value of m is not modified because m contains the result of the calculation and not the calculation itself ;-)
  • Jack Danger said: Thanks for the proofreading Mourad, you're a big help :-)
  • Daniel Fischer said: Wow, that is an amazing pattern to uphold to. So many plusses all around it! Thanks for sharing that Danger!
  • Ryan Bates said: Nice tip. I usually move it into a 2nd method prefixed with "calculate". In this example I would create a new private method called "calculate_current_advertising_balance". This way the "current_advertising_balance" method simply handles the caching and calling of the calculate method.
  • Pawel said: This is nice, but Pickaxe SE (page 374) has IMHO nicer solution. class MyKlass def cpu_intensive_method #... end once :cpu_intensive_method end "once" is a class method which encapsulates given method in another method which do caching. You can see "once" method definition in Pickaxe, authors say it is used in "date" module.
  • Phil said: I just used this at work today. I wonder if you could encapsulate it with some metaprogramming like Pawel mentioned. One thing that's missing is a current_advertising_balance! method that discards the cached value and forces a recalculation.
  • Jack Danger said: Phil: I originally developed this as a gem thinking it might be useful: http://github.com/JackDanger/simple_memoize/tree/master I hadn't considered the invalidating bang method - that's a good idea.
  • ara.t.howard said: this is all done in my attributes lib, which is battle hardened and minimal to boot. if you do class C; attribute(:a){ Time.now } end then c = C.new c.a c.a calculates just once (the block is instance eval'd) and c = C.new c.a c.a! re-instance_evals the initialization block # gem install attributes
blog comments powered by Disqus