Don't Forget About to_s
In a
previous article I talked using inspect
to make using
IRb as easy as possible and it reminded me that there is another method
that is implicitly used by every Ruby developer, possibly hundreds of
times a day, but which few ever define on their objects. That method is
to_s
.
Every time you see string interpolation (whenever you use
#{object}
in a string, or <%= object %>
in Rails) you’re using to_s, every time you use puts
you’re using to_s
. And yet implementing
to_s
is often never done.
Before I start let’s use an example class of a Person with a first name, last name, date of birth and postal address.
1 class Person 2 attr_accessor :first_name, :last_name, :date_of_birth, :postal_address 3 end
Why
Why in fact do I need to implement to_s
anyway? The
implied use of to_s
in string interpolation and with
puts
is mainly for readability. It allows us to directly
refer to an object in a certain context and not have to describe
exactly what we want in that context. With string interpolation
1 <formset title="Edit details for <%= person %>">
will read a lot better than
1 <formset title="Edit details for <%= person.first_name %> <%= person.last_name %>">
What
In the above example I used a simple to_s
implementation
1 class Person 2 def to_s 3 "#{ @first_name } #{ @last_name }" 4 end 5 end
and in general, implementations of to_s
should provide
a simple label or representation of your data, suitable for end
users, output on a single line. How’s that for output
restriction!
In our
previous article dealing with inspect
I showed that
Date#inspect
was pretty opaque to someone not interested
in debugging the underlying data structures, however
Date#to_s
is transparent:
1 >> Date.today 2 => #<Date: 4909485/2,0,2299161> 3 >> Date.today.to_s 4 => "2008-10-03"
as we would expect from a format that is designed to be totally human readable.
How
Ruby’s builtin data structures, Array
and
Hash
, output all their contained data in a single line
when to_s
is called on them.
1 >> [1,2,3,4].to_s 2 => "1234" # From Ruby 1.9 onwards this will be "[1,2,3,4]" 3 >> {:a => 1, :b => 2}.to_s 4 => "a1b2" # And this will be "{:a => 1, :b => 2}"
For more complex data types outputting the entire object into a
short string representation is not always possible so instead
it’s better to refer to it by a natural identifier or labelling
of some sort. For our Person
class described earlier we
just used the first and last names of the person represented by the
object and to make it more specific we could add the age of the person
in years like so
1 class Person 2 def to_s 3 "#{ @first_name } #{ @last_name }(#{ Date.today.year - @date_of_birth.year + ( @date_of_birth.yday < Date.today.yday ? 1 : 0 ) })" 4 end 5 end
That implementation produces the following output on line 4:
1 >> p 2 => #<Person:0xb7a9a710 @first_name="Joe", @postal_address="10 Main Street\nSmalltown\nUSA", @date_of_birth=#<Date: 4885443/2,0,2299161>, @last_name="Public"> 3 >> p.to_s 4 => "Joe Public(33)"
Even though the postal_address
is defined for the
person it’s pointless trying to include it. To format an address
correctly would take multiple lines and wouldn’t make sense being
included on a single line output. In the case of an object like
Person
an identifier of the object, such as the first and
last names, is sufficient. We added the age of the person to show how
other data might be included but in my opinion it’s not
required.
Because the requirements of to_s
can change depending
on the context of where it’s used, you might want to implement
to_s
so it takes an argument specifying the output format
you want, which is what Rails does in its monkeypatching of the
Date and
Time classes. It’s pretty easy to implement yourself:
1 class Person 2 def to_s( format = nil ) 3 "#{ @first_name } #{ @last_name }" + 4 case format 5 when :with_age 6 "(#{ Date.today.year - @date_of_birth.year + ( @date_of_birth.yday < Date.today.yday ? 1 : 0 ) })" 7 when :with_date_of_birth 8 "(#{ @date_of_birth })" 9 else 10 "" 11 end 12 end 13 end
1 >> p.to_s 2 => "Joe Public" 3 >> p.to_s( :with_age ) 4 => "Joe Public(33)" 5 >> p.to_s( :with_date_of_birth ) 6 => "Joe Public(1975-11-05)"
As with implementing inspect
, when deciding how to
implement to_s
you need to be aware where you most use
string interpolation and what you need from your object to either
describe it in a compact form or identify it by some labelling.
Farrel Lifson is a lead developer at Aimred.