Getting The Most Out of IRb With Inspect
IRb is Interactive Ruby, a Ruby shell that installs along
with Ruby allowing you to write and run Ruby progams interactively (try
it out by typing irb
on the command line or on the web). IRb forms the basis for the
Rails console and is used in a number of other debugging platforms and
consoles. In other words, it’s pretty important, and any
Ruby/Rails developer will use it daily.
In this article we’ll discuss tips and strategies to get the
most out of your IRb session. And all it requires is to make sure that
one single method, inspect
is implemented correctly.
Let’s first look at the process IRb follows when commands are
entered and evaluated. We should also note that we run IRb with the
--simple-prompt
option for brevity so in the following IRb
sessions input is on lines marked with >>
while the
evaluated output from IRb is on lines prepended with
=>
.
IRb’s Execution Cycle
Here’s a snippet from an IRb session
1 >> colours = { :red = "#FF0000", :green => "#00FF00", :blue => "#0000FF" } 2 => {:red = "#FF0000", :green => "#00FF00", :blue => "#0000FF"}
Here are the steps IRb follows:
- The expression on line 1 is evaluated; in this case a new Hash
object is created and assigned to the variable
colours
. The result of this assignment is the created hash and this is returned to IRb. - IRb calls the
inspect
on the hash which returns a string representation of the hash:"{:red = "#FF0000", :green => "#00FF00", :blue => "#0000FF"}"
- The string is output on line 2 using
puts
. Notice how the space betweent the{
and:red
which was present in the input is not present in the output, which shows this is a generated string representation of the hash and not just a copy and paste of what I typed in.
Now for the majority of built in Ruby classes inspect
produces representation that is logical. For instance
"Aimred".inspect
produces the string
"\"Aimred\""
so that when it’s output with
puts
the quotation marks are preserved in the output like
so
1 >>"Aimred" 2 =>"Aimred" # puts "\"Aimred\""
As we saw previously, inspect called on a hash produces a representation of a hash that is almost exactly what we typed in, but when it comes to custom classes, things don’t look so pretty. Let’s use an example of a temperature monitor that has been taking readings for the past 50 days.
1 class TemperatureMonitor 2 attr_accessor :name, :lattitude, :longitude, 3 :altitude, :readings 4 5 def initialize( name, lattitude, longitude, altitude ) 6 @name = name 7 @lattitude = lattitude 8 @longitude = longitude 9 @altitude = altitude 10 @readings = [] 11 end 12 end 13 14 class Reading 15 attr_accessor :date, :temperature 16 17 def initialize( date, temperature ) 18 @date = date 19 @temperature = temperature 20 end 21 end 22 23 tm = TemperatureMonitor.new( "Cape Town International Airport", 24 -33.96, 18.59, 42 ) 25 26 (Date.today - 50 .. Date.today).inject( tm ) do |monitor, date| 27 monitor.readings << Reading.new( date, rand(10) + 15 ) 28 monitor 29 end
And this is what it looks like in IRb
1 => #<TemperatureMonitor:0xb7b85d78 @altitude=42, @longitude=18.59, @lattitude=-33.96, @readings=[#<Reading:0xb7b8597c @temperature=15, @date=#<Date: 4909355/2,0,2299161>>, #<Reading:0xb7b85918 @temperature=19, @date=#<Date: 4909357/2,0,2299161>>, #<Reading:0xb7b858a0 @temperature=16, @date=#<Date: 4909359/2,0,2299161>>, #<Reading:0xb7b85828 @temperature=18, @date=#<Date: 4909361/2,0,2299161>>, #<Reading:0xb7b857b0 @temperature=20, @date=#<Date: 4909363/2,0,2299161>>, #<Reading:0xb7b85738 @temperature=21, @date=#<Date: 4909365/2,0,2299161>>, #<Reading:0xb7b856c0 @temperature=23, @date=#<Date: 4909367/2,0,2299161>>, #<Reading:0xb7b85648 @temperature=18, @date=#<Date: 4909369/2,0,2299161>>, #<Reading:0xb7b855d0 @temperature=17, @date=#<Date: 4909371/2,0,2299161>>, #<Reading:0xb7b85558 @temperature=22, @date=#<Date: 4909373/2,0,2299161>>, #<Reading:0xb7b854e0 @temperature=24, @date=#<Date: 4909375/2,0,2299161>>, #<Reading:0xb7b85468 @temperature=24, @date=#<Date: 4909377/2,0,2299161>>, #<Reading:0xb7b853f0 @temperature=16, @date=#<Date: 4909379/2,0,2299161>>, #<Reading:0xb7b85378 @temperature=15, @date=#<Date: 4909381/2,0,2299161>>, #<Reading:0xb7b85300 @temperature=16, @date=#<Date: 4909383/2,0,2299161>>, #<Reading:0xb7b85288 @temperature=23, @date=#<Date: 4909385/2,0,2299161>>, #<Reading:0xb7b85210 @temperature=22, @date=#<Date: 4909387/2,0,2299161>>, #<Reading:0xb7b85198 @temperature=19, @date=#<Date: 4909389/2,0,2299161>>, #<Reading:0xb7b85120 @temperature=17, @date=#<Date: 4909391/2,0,2299161>>, #<Reading:0xb7b850a8 @temperature=15, @date=#<Date: 4909393/2,0,2299161>>, #<Reading:0xb7b85030 @temperature=17, @date=#<Date: 4909395/2,0,2299161>>, #<Reading:0xb7b84fb8 @temperature=22, @date=#<Date: 4909397/2,0,2299161>>, #<Reading:0xb7b84f40 @temperature=22, @date=#<Date: 4909399/2,0,2299161>>, #<Reading:0xb7b84ec8 @temperature=20, @date=#<Date: 4909401/2,0,2299161>>, #<Reading:0xb7b84e50 @temperature=20, @date=#<Date: 4909403/2,0,2299161>>, #<Reading:0xb7b84dd8 @temperature=17, @date=#<Date: 4909405/2,0,2299161>>, #<Reading:0xb7b84d60 @temperature=16, @date=#<Date: 4909407/2,0,2299161>>, #<Reading:0xb7b84ce8 @temperature=22, @date=#<Date: 4909409/2,0,2299161>>, #<Reading:0xb7b84c70 @temperature=15, @date=#<Date: 4909411/2,0,2299161>>, #<Reading:0xb7b84bf8 @temperature=23, @date=#<Date: 4909413/2,0,2299161>>, #<Reading:0xb7b84b80 @temperature=17, @date=#<Date: 4909415/2,0,2299161>>, #<Reading:0xb7b84b08 @temperature=20, @date=#<Date: 4909417/2,0,2299161>>, #<Reading:0xb7b84a90 @temperature=18, @date=#<Date: 4909419/2,0,2299161>>, #<Reading:0xb7b84a18 @temperature=19, @date=#<Date: 4909421/2,0,2299161>>, #<Reading:0xb7b849a0 @temperature=20, @date=#<Date: 4909423/2,0,2299161>>, #<Reading:0xb7b84928 @temperature=17, @date=#<Date: 4909425/2,0,2299161>>, #<Reading:0xb7b848b0 @temperature=22, @date=#<Date: 4909427/2,0,2299161>>, #<Reading:0xb7b84838 @temperature=19, @date=#<Date: 4909429/2,0,2299161>>, #<Reading:0xb7b847c0 @temperature=23, @date=#<Date: 4909431/2,0,2299161>>, #<Reading:0xb7b84748 @temperature=19, @date=#<Date: 4909433/2,0,2299161>>, #<Reading:0xb7b846d0 @temperature=23, @date=#<Date: 4909435/2,0,2299161>>, #<Reading:0xb7b84658 @temperature=20, @date=#<Date: 4909437/2,0,2299161>>, #<Reading:0xb7b845e0 @temperature=16, @date=#<Date: 4909439/2,0,2299161>>, #<Reading:0xb7b84568 @temperature=15, @date=#<Date: 4909441/2,0,2299161>>, #<Reading:0xb7b844f0 @temperature=15, @date=#<Date: 4909443/2,0,2299161>>, #<Reading:0xb7b84478 @temperature=23, @date=#<Date: 4909445/2,0,2299161>>, #<Reading:0xb7b84400 @temperature=22, @date=#<Date: 4909447/2,0,2299161>>, #<Reading:0xb7b84388 @temperature=22, @date=#<Date: 4909449/2,0,2299161>>, #<Reading:0xb7b84310 @temperature=22, @date=#<Date: 4909451/2,0,2299161>>, #<Reading:0xb7b84298 @temperature=15, @date=#<Date: 4909453/2,0,2299161>>, #<Reading:0xb7b84220 @temperature=24, @date=#<Date: 4909455/2,0,2299161>>], @name="Cape Town International Airport">
A bit confusing isn’t it? Unfortunately Date
is
one of the few classes in the standard library that does not have a
decent inspect
method defined by default. Let’s look
at what we need to get some usable output.
Defining Your Inspect Method
So before we write an inspect method, what are the goals that we should try and achieve?
- Human readable: everything that gets output should be human
readable. The output of
Date#inspect
, as shown above, is not. - As little scrolling as possible: as much information as possible should be available on the screen without having to scroll through pages of output.
Make Opaque Objects Transparent
As we saw above, the result of inspect
called on a
Date
object is
1 >> #<Date: 4909453/2,0,2299161>
which is not very clear. If you’re actually debugging the
Date
class, it makes some sense; it shows the
Astronomical Julian Day Number, the Offset and the
Day of Calendar Reform. To everyone else ,however, those
values are close to useless and so it would make a lot of sense to
override Date#inspect
.
1 >> date = Date.today 2 => #<Date: 4909457/2,0,2299161> 3 >> class Date 4 >> def inspect 5 >> "#{ self.day }/#{ self.month }/#{ self.year }" 6 >> end 7 >> end 8 >> date 9 => 19/9/2008
Comparing the output on line 2 and 9 of IRb session above shows the
difference between the original version of Date#inspect
and our overriden version defined in lines 4 – 6. For a normal
developer using the Date class, the format on line 9 would be much
preferred.
Keep Within The Output Limits
For the vast majority, IRb and the applications based on it are run in a terminal or console, which means the initial size of your output window is 80×24 characters. That sets a target for what we would like to achieve: as much information as possible formatted into an area of 80×24 characters.
In cases where data is not going to fit in the required space, it is
often better to expand vertically than horizontally. The human brain is
much more accustomed to scanning for data in a vertical direction than
horizontally. The output for TemperatureMonitor
violates
this by having a long stream of Reading
objects, making it
hard to look and compare temperatures for dates that are separated by
more than one or two days.
Make Long Attributes Shorter
Sometimes you don’t need to show everything. Particularly with
array like strutctures (queues, stacks), we tend to be only interested
in items at the beginning or end of the array. With our
TemperatureMonitor
we would tend to look at the most
recent temperatures so in this case it would make sense for us to only
show the 5 most recent readings. If we need to see all the readings we,
can display the $tm.readings
attribute directly.
If you’d prefer not to limit the amount of data displayed, consider putting the more commonly needed attributes at the end of the output; that way they will always be on the screen after the contents of an object are output.
Putting It All Together
Using the principles we have listed above, let’s write an
inspect method for our TemperatureMonitor
to achieve the
goals we wanted (human readable, no scrolling) using the techniques we
discussed.
First, here’s the inspect for the Reading
class
which uses the overridden Date#inspect
method:
1 class Reading 2 def inspect 3 "#{ @date.inspect }: #{ @temperature }C" 4 end 5 end
and the inspect for the TemperatureMonitor
class:
1 class TemperatureMonitor 2 def inspect 3 inspect_result= "Temperature Monitor: #{ @name }\n"+ 4 "\tLattitude: #{ @lattitude } Longitude: #{ @longitude } Altitude: #{ @altitude }m\n"+ 5 "\tLast 5 temperature readings(#{ @readings.size } total):" 6 @readings[-5, 5].inject( inspect_result ) do |ir, reading| 7 ir << "\n\t\t#{ reading.inspect }" 8 end 9 end 10 end
Now compare what we originally had:
1 => #<TemperatureMonitor:0xb7b85d78 @altitude=42, @longitude=18.59, @lattitude=-33.96, @readings=[#<Reading:0xb7b8597c @temperature=15, @date=#<Date: 4909355/2,0,2299161>>, #<Reading:0xb7b85918 @temperature=19, @date=#<Date: 4909357/2,0,2299161>>, #<Reading:0xb7b858a0 @temperature=16, @date=#<Date: 4909359/2,0,2299161>>, #<Reading:0xb7b85828 @temperature=18, @date=#<Date: 4909361/2,0,2299161>>, #<Reading:0xb7b857b0 @temperature=20, @date=#<Date: 4909363/2,0,2299161>>, #<Reading:0xb7b85738 @temperature=21, @date=#<Date: 4909365/2,0,2299161>>, #<Reading:0xb7b856c0 @temperature=23, @date=#<Date: 4909367/2,0,2299161>>, #<Reading:0xb7b85648 @temperature=18, @date=#<Date: 4909369/2,0,2299161>>, #<Reading:0xb7b855d0 @temperature=17, @date=#<Date: 4909371/2,0,2299161>>, #<Reading:0xb7b85558 @temperature=22, @date=#<Date: 4909373/2,0,2299161>>, #<Reading:0xb7b854e0 @temperature=24, @date=#<Date: 4909375/2,0,2299161>>, #<Reading:0xb7b85468 @temperature=24, @date=#<Date: 4909377/2,0,2299161>>, #<Reading:0xb7b853f0 @temperature=16, @date=#<Date: 4909379/2,0,2299161>>, #<Reading:0xb7b85378 @temperature=15, @date=#<Date: 4909381/2,0,2299161>>, #<Reading:0xb7b85300 @temperature=16, @date=#<Date: 4909383/2,0,2299161>>, #<Reading:0xb7b85288 @temperature=23, @date=#<Date: 4909385/2,0,2299161>>, #<Reading:0xb7b85210 @temperature=22, @date=#<Date: 4909387/2,0,2299161>>, #<Reading:0xb7b85198 @temperature=19, @date=#<Date: 4909389/2,0,2299161>>, #<Reading:0xb7b85120 @temperature=17, @date=#<Date: 4909391/2,0,2299161>>, #<Reading:0xb7b850a8 @temperature=15, @date=#<Date: 4909393/2,0,2299161>>, #<Reading:0xb7b85030 @temperature=17, @date=#<Date: 4909395/2,0,2299161>>, #<Reading:0xb7b84fb8 @temperature=22, @date=#<Date: 4909397/2,0,2299161>>, #<Reading:0xb7b84f40 @temperature=22, @date=#<Date: 4909399/2,0,2299161>>, #<Reading:0xb7b84ec8 @temperature=20, @date=#<Date: 4909401/2,0,2299161>>, #<Reading:0xb7b84e50 @temperature=20, @date=#<Date: 4909403/2,0,2299161>>, #<Reading:0xb7b84dd8 @temperature=17, @date=#<Date: 4909405/2,0,2299161>>, #<Reading:0xb7b84d60 @temperature=16, @date=#<Date: 4909407/2,0,2299161>>, #<Reading:0xb7b84ce8 @temperature=22, @date=#<Date: 4909409/2,0,2299161>>, #<Reading:0xb7b84c70 @temperature=15, @date=#<Date: 4909411/2,0,2299161>>, #<Reading:0xb7b84bf8 @temperature=23, @date=#<Date: 4909413/2,0,2299161>>, #<Reading:0xb7b84b80 @temperature=17, @date=#<Date: 4909415/2,0,2299161>>, #<Reading:0xb7b84b08 @temperature=20, @date=#<Date: 4909417/2,0,2299161>>, #<Reading:0xb7b84a90 @temperature=18, @date=#<Date: 4909419/2,0,2299161>>, #<Reading:0xb7b84a18 @temperature=19, @date=#<Date: 4909421/2,0,2299161>>, #<Reading:0xb7b849a0 @temperature=20, @date=#<Date: 4909423/2,0,2299161>>, #<Reading:0xb7b84928 @temperature=17, @date=#<Date: 4909425/2,0,2299161>>, #<Reading:0xb7b848b0 @temperature=22, @date=#<Date: 4909427/2,0,2299161>>, #<Reading:0xb7b84838 @temperature=19, @date=#<Date: 4909429/2,0,2299161>>, #<Reading:0xb7b847c0 @temperature=23, @date=#<Date: 4909431/2,0,2299161>>, #<Reading:0xb7b84748 @temperature=19, @date=#<Date: 4909433/2,0,2299161>>, #<Reading:0xb7b846d0 @temperature=23, @date=#<Date: 4909435/2,0,2299161>>, #<Reading:0xb7b84658 @temperature=20, @date=#<Date: 4909437/2,0,2299161>>, #<Reading:0xb7b845e0 @temperature=16, @date=#<Date: 4909439/2,0,2299161>>, #<Reading:0xb7b84568 @temperature=15, @date=#<Date: 4909441/2,0,2299161>>, #<Reading:0xb7b844f0 @temperature=15, @date=#<Date: 4909443/2,0,2299161>>, #<Reading:0xb7b84478 @temperature=23, @date=#<Date: 4909445/2,0,2299161>>, #<Reading:0xb7b84400 @temperature=22, @date=#<Date: 4909447/2,0,2299161>>, #<Reading:0xb7b84388 @temperature=22, @date=#<Date: 4909449/2,0,2299161>>, #<Reading:0xb7b84310 @temperature=22, @date=#<Date: 4909451/2,0,2299161>>, #<Reading:0xb7b84298 @temperature=15, @date=#<Date: 4909453/2,0,2299161>>, #<Reading:0xb7b84220 @temperature=24, @date=#<Date: 4909455/2,0,2299161>>], @name="Cape Town International Airport">
with what our overridden TemperatureMonitor#inspect
produces:
1 => Temperature Monitor: Cape Town International Airport 2 Lattitude: -33.96 Longitude: 18.59 Altitude: 42m 3 Last 5 temperature readings(51 total): 4 17/9/2008: 18C 5 18/9/2008: 23C 6 19/9/2008: 22C 7 20/9/2008: 16C 8 21/9/2008: 15C
That is a lot easier to read; all the data is transparent and it fits easily in a restricted ouput space. Even a novice user would be able to tell you straight away what this object represents and the values of its various attributes are.
Using Already Defined Methods
Sometimes you might not have the time to write a new inspect or the
object you’re interested is small enough that its default
inspect
implementation is sufficient. But to make it even
better, there are a number of already defined APIs available which can
quickly make objects morre readable. We’ll use a simple
Book
class with an author
,
title
, publication_date
and
publisher
.
Pretty Print
Ruby comes with a module called PrettyPrint which does just
what it says it does; pretty prints output. It provides a
pp
method which in IRb prints every attribute on a new
line, which makes things more readable:
1 >> require 'pp' 2 >> pp book 3 #<Book:0xb7c77858 4 @author="Frank Herbert", 5 @publication_date=#<Date: 4877523/2,0,2299161>, 6 @publisher="Chilton Books", 7 @title="Dune"> 8 => nil
You’ll notice that pp
does not reformat data so
our Date
objects are still opaque to most users.
YAML
YAML (Yet Another Markup Language) is
a markup language designed to be human readable and editable, and is
quite popular in the Ruby community finding usage in Rails as a
database configuration file format and elsewhere. By aliasing
inspect
to to_yaml
we get a pretty nice
output:
1 >> require 'yaml' 2 >> book 3 => --- !ruby/object:Book 4 author: Frank Herbert 5 publication_date: 1965-01-01 6 publisher: Chilton Books 7 title: Dune
Here the Date
object is reformatted by YAML to be human
readable. The only drawback of using YAML is that it is intended to
serialize the entire object so in a case like the
TemperatureMonitor
, where we have a large number of
temperature readings, they all will be output to the screen,
potentially causing a lot of scrolling. It also tends to put every
attribute on a separate line so you can’t for instance arrange
data in a tabular format like we did when we created our own
inspect
implementation.
Concluding Remarks
Writing a decent inspect
implement can save you a lot
of frustration and eyeball strain in the long run. It also makes like
easier for users of your system who are not proficient Ruby users.
Don’t take this article as the one true way to implement
inspect
, you should select whatever works best for you,
your developers and your users.
Farrel Lifson is a lead developer at Aimred.