In Progress
Unit 1, Lesson 1
In Progress

Inline Assignment

Have you ever programmed in C/C++? Back when I used those languages for my day job, it often seemed like half of any given function was taken up with variable declarations. Ruby, with its block-oriented syntax, does away with most of this declaration boilerplate. But there are still a few cases where you can find yourself needing to assign a variable on a separate line, only to use it on just one other line of code.

In this episode, you’ll learn a syntactic trick that lets you create a new object and give a name to it at the same time, without interrupting the visual flow of your code. Enjoy!

Video transcript & code

I've used the idiom of inline assignment in passing in a few different episodes. Today I thought I'd briefly shine a spotlight on it.

Let's start with looping example. Say I want to find out how many lines there are in the book I'm working on. And just for the sake of example, let's say I'm very concerned about memory performance, so I want to do the calculation in a streaming style, rather than sucking the whole file in at once.

We can do this by reading the book one fixed-size chunk at a time. #read with an integer argument will read at most that number of bytes and return nil once it gets to the end of input. So to continue forward we start a loop that will continue until the current chunk is nil. Inside, we increment the line counter by the number of newline characters in the current chunk. Then we read the next chunk.

book = open('../../books/confident-ruby/confident-ruby.org')
lines = 0
chunk = book.read(1024)
while chunk
  lines += chunk.count("\n")
  chunk = book.read(1024)
end
puts lines

4713

This code is kind of gross because the file reading code is executed twice. To fix this, we can instead move the assignment inline, into the loop's condition clause. This line now does three things: it reads up to 1k of the file. Then it assigns the resulting string (or nil value) to the chunk variable. The value of the assignment is the value being assigned, which the while then checks to see if it should continue.

book = open('../../books/confident-ruby/confident-ruby.org')
lines = 0
while chunk = book.read(1024)
  lines += chunk.count("\n")
end
puts lines
4713

Not only is this shorter, and more DRY, it has the agreeable property that the use of the chunk variable is neatly contained within the loop it pertains to. If we were to ever remove the loop, we wouldn't be leaving a dangling variable behind.

book = open('../../books/confident-ruby/confident-ruby.org')
lines = 0

puts lines

There's one objection we can make to this idiom. Because it is more common to compare values in a loop condition than it is to assign them, someone reading the code might be momentarily tripped up by this line, thinking that perhaps we meant to use a double equals. Some code quality analysis tools will also flag this as a possible mistake.

while chunk == book.read(1024)

A convention that some Ruby programmers use to avoid this confusion is to always surround loop conditions containing inline assignment with a set of parentheses. This doesn't change the semantics of the code at all, but acts as a flag to future readers to say "yes, I meant to do that". Of course, they have to be aware of this convention in order to benefit from it.

while(chunk = book.read(1024))
  lines += chunk.count("\n")
end

Another place I like to use inline assignments is in tests. Consider this RSpec test for a class that records a visit to a website. Each visit should be timestamped with the current time when it is instantiated. In order to test this, we assign an expected time using a known time value, and stub out the #now method on the Time class to return that value. Then we instantiate a SiteVisit object and check that the timestamp equals the expected time.

require 'rspec/autorun'
describe SiteVisit do
  it 'sets timestamp to the current time' do
    expected_time = Time.new(2013, 1, 1)
    Time.stub(now: expected_time)
    visit = SiteVisit.new
    visit.timestamp.should eq(expected_time)
  end
end

The implementation of SiteVisit is trivial.

class SiteVisit
  attr_reader :timestamp
  def initialize
    @timestamp = Time.now
  end
end

We can save some typing in this test by inlining the assignment of the expected_time into the definition of the Time.now stub.

require 'rspec/autorun'
describe SiteVisit do
  it 'sets timestamp to the current time' do
    Time.stub(now: expected_time = Time.new(2013, 1, 1))
    visit = SiteVisit.new
    visit.timestamp.should eq(expected_time)
  end
end

I wouldn't use this in a deeply nested stub definition. For that matter, I try not to ever have deeply nested stubs. But for quickly naming the return value of one stubbed method, I find this idiom to be convenient and still readable.

That's it for today. Happy hacking!

Responses