In Progress
Unit 1, Lesson 21
In Progress

Multiline Memoize

Sometimes you need to treat multiple Ruby statements as if they were a single expression, but you don’t want to create a whole new method for it. Ruby has a syntax for this, but it’s surprisingly little-known. In this episode you’ll learn how to apply this Ruby idiom, using the practical example of memoizing a method.

Video transcript & code

Here's a Ruby idiom you're probably familiar with. We have a method which returns a value. The return value requires an expensive operation to discover.

def user_profile
  some_service.user_profile_for(@username)
end

There's no need to re-calculate it every time. So we memoize the result using an instance variable and the or-equals operator.

def user_profile
  @user_profile ||= some_service.user_profile_for(@username)
end

This works great for simple methods. But sometimes we run across a method whose return value we'd like to memoize, but the value requires multiple lines of code to generate. Here's an example from some code I wrote.

def episode_list
  connection = Faraday.new do |conn|
    conn.basic_auth login, password
    conn.adapter :net_http
  end
  response   = connection.get("https://rubytapas.dpdcart.com/feed")
  response.status == 200 or raise "Unable to retrieve feed"
  body       = response.body
  doc        = Nokogiri::XML(body)
  items      = doc.xpath("//item")
  episodes   = items.map {|item|
    {
      title: item.at_xpath("./title").text
    }
  }
end

The details of the method aren't important. the important thing is that this code makes an HTTP request, which we'd like to avoid making more than once. And the result of the request requires multiple lines of processing before the value is ready to be returned.

There's actually a very simple way to memoize this code, and it involves begin blocks. Let's talk about begin blocks for a minute.

You're probably most familiar with begin blocks as a way to introduce a zone of code in which exceptions will be rescued.

begin
  # ...
rescue SomeException => e
  # ...
end

But that's no their only use. You might recall back in Episode #73 we used a begin block to implement a "do-while" loop in ruby. We wrapped the begin block around a sequence of statements, turning them into a logical unit. To which we could then apply the while statement modifier.

begin  
  file.seek(next_chunk_offset, IO::SEEK_END)
  chunk_start_offset = file.tell
  chunk              = file.read(chunk_size)
  newline_count     += chunk.to_s.chars.count("\n")
  next_chunk_offset -= chunk_size
end while chunk && chunk_start_offset > 0 && newline_count <= 10

In fact, the begin block is a general purpose language feature for grouping statements together. Statements inside the block are executed sequentially just as they are anywhere else in Ruby code, and the return value of the block is the value of the final statement. We can see this if assign the result of a begin block to a variable, and then perform a series of operations inside the block. The value of the last statement is the value returned.

result = begin
           a = %W[foo bar baz]
           a.delete("foo")
           a.delete("bar")
           a
         end
result                          # => ["baz"]

Incidentally, in case you were wondering, begin blocks do not introduce a new variable scope. We can see this if we assign a variable inside a begin block and then read the variable outside the block.

begin
  a = 23
end
a                               # => 23

Getting back to our memoization example, we can memoize the entire body of the method by enclosing it in a begin block and assigning the result of the block to an instance variable.

def episode_list
  @episode_list ||= begin
    connection = Faraday.new do |conn|
      conn.basic_auth login, password
      conn.adapter :net_http
    end
    response   = connection.get("https://rubytapas.dpdcart.com/feed")
    response.status == 200 or raise "Unable to retrieve feed"
    body       = response.body
    doc        = Nokogiri::XML(body)
    items      = doc.xpath("//item")
    episodes   = items.map {|item|
      {
        title: item.at_xpath("./title").text
      }
    }
  end
end

Now the episode list will only be calculated once.

However, now that you know how to do this, I have to confess that I almost never use this technique. I'll tell you why.

At least half the time when I introduce a begin block for multiline memoization, I late realize that I also want a way to invoke the method with no memoization. In certain cases, I want to be sure that I'm getting a fresh value, not a cached one. Or that I'm forcing the request to be made.

So what I do instead is this: I extract the entire body of the method into a new method. I typically give this new method the same name as the old one, prefixed with get_. This emphasizes the fact that this new method will always take action, rather than returning a cached value.

Then I update the original method to do a single-line memoized send to the extracted method.

def episode_list
  @episode_list ||= get_episode_list
end

def get_episode_list
  connection = Faraday.new do |conn|
    conn.basic_auth login, password
    conn.adapter :net_http
  end
  response   = connection.get("https://rubytapas.dpdcart.com/feed")
  response.status == 200 or raise "Unable to retrieve feed"
  body       = response.body
  doc        = Nokogiri::XML(body)
  items      = doc.xpath("//item")
  episodes   = items.map {|item|
    {
      title: item.at_xpath("./title").text
    }
  }
end

This gives me my memoized method as well as a way to bypass memoization, should I need to. And it has the added bonus that I haven't introduced a new level of nesting into a method.

So there you go: two different techniques for memoizing complex methods. Happy hacking!

Responses