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
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
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!