Aimred Developer Blog September 2008 Archive

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:

  1. 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.
  2. IRb calls the inspect on the hash which returns a string representation of the hash: "{:red = "#FF0000", :green => "#00FF00", :blue => "#0000FF"}"
  3. 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.

Proc Equality Implemented In Ruby 1.9!

Wow! Matz (Yukuhiro Matsumoto, the father of Ruby) has implemented the changes we proposed to make Proc#=== be the same as Proc#call in Ruby 1.9! We never thought in a million years it would actually ever find its way into the language.

And to make it even better, Dave Thomas (the author of the book on Ruby, Programming Ruby) shows how to use our suggestion with Proc#curry for even more elegant code.

September Cape Town Ruby Brigade Meeting

The details for the next meeting of the Cape Town Ruby Brigade are

  • Topic: Andy Duncan will discuss the Rails based system built for NGO use during the recent Xenophobic attacks
  • Date: Wednesday, 10th September 2008
  • Time: 19:00
  • Venue: Bandwidth Barn, 125 Buitengracht Street

More information at the meeting page.

New On Our Bookshelf

  • The Ruby Programming Language – David Flanagan & Yukihiro Matsumoto
  • The Rails Way 2nd Edition – Obie Fernandez et al.

About Aimred

Aimred is a specialist Ruby and Ruby on Rails development house and consultancy based in Cape Town, South Africa.

We provide Ruby and Ruby on Rails development, consulting and training services to businesses and organisations of all sizes. If you want to find out how we can help you, contact us at info@aimred.com.