2
OCT2008
Interpolation and Statements
I still cringe anytime I see code like:
"1 + 2 = " + (1 + 2).to_s # => "1 + 2 = 3"
Some books even advocate the above, which is a real shame for Ruby.
I imagine most of you know that you can rewrite the above to use String
interpolation:
"1 + 2 = #{1 + 2}" # => "1 + 2 = 3"
Let's think about that simple code a little bit more than we usually do though. What's really going on here? Obviously #{ … }
inserts the result of the embedded code in the String
, but it's important to realize that it also calls to_s()
on that result to make it fit in the String
.
We can really make use of that knowledge if we try. Here's an example:
Name = Struct.new(:first, :last) do
def full
"#{first} #{last}".strip # trick 1
end
alias_method :to_s, :full # trick 2
end
Name.new("James").full # => "James"
Name.new(:James, :Gray).full # => "James Gray"
"My name is #{Name.new('James', 'Gray')}." # => "My name is James Gray."
I've built a trivial data class for managing names here. In that, I've tried to make use of interpolation to the fullest.
First, I needed to be able to build a reasonable representation of a full name no matter what data I have. My first example shows how this might play out when I only have a first name. That means first()
will return "James"
and last()
will return nil
. There are several ways to deal with this, but I chose what I consider to be one of the easiest.
I want something like a String
for first and last name. Interpolation pretty much enforces this for me, since it automatically calls to_s()
on the interpolated values. It just so happens that nil.to_s
is ""
and following that up with a simple strip()
will remove any excess space due to missing names.
You can also see from the second example that I don't have to be strictly using String
objects for the names. Anything with a reasonable to_s()
will do.
Taking that one step further, the third example shows that even Name
itself can benefit from a reasonable to_s()
. This allows me to drop the full object right into any old String
. You can take this really far by just adding sensible to_s()
definitions to all kinds of objects. You could have objects representing data changes dropping themselves right into audit logs, have game move classes automatically serialize themselves for transport over a network protocol, or anything else you can think of.
Hopefully I've made that point now. The implicit to_s()
of String
interpolation is nice. Now let's look at another aspect of Ruby.
Many languages go to great lengths to distinguish between concepts like a statement and an expression, what each of those does, and where they can appear. Thankfully, Ruby doesn't much care.
What does that mean for us? Well, just about everything evaluates to something. You can kind of think of it as everything having a return value. (It's not really a return value in the case of something like a conditional, but I think that description makes for a good visual of what we are talking about here.) Let me show some examples:
a = b = 2
a # => 2
b # => 2
num = rand(20) - 10
type = if num >= 0 then :positive
else :negative
end
[num, type] # => [-3, :negative]
age = rand(100)
group = case age
when 0..18 then :child
when 19..65 then :adult
when 66..100 then :senior
end
[age, group] # => [44, :adult]
not_run = if false
# ...
end
not_run # => nil
def do_nothing; end
not_run = do_nothing
not_run # => nil
Hopefully something in there surprises you. Take special note of the last two which pretty much show that Ruby tends to just go with nil
when there's nothing else for a construct to evaluate to. We can use that.
Now, let's throw those two concepts together. Interpolation calls to_s()
on the results of any code and we now know that most any code will have a result. That means we can write code like this:
def pluralize(count, singular)
"#{count} #{singular}#{'s' unless count == 1}"
end
3.times { |n| puts pluralize(n, "trick") }
# >> 0 tricks
# >> 1 trick
# >> 2 tricks
Obviously, this is a pretty dumb method compared to the similar functionality in ActiveSupport
and other libraries, but check out that last interpolation. I dumped a full unless
right in the String
. If it triggers, it will evaluate to a simple 's'
, which may be an unusual use of a conditional but is exactly what we need here. When it doesn't trigger, we know Ruby will punt with nil
and nil.to_s
is nothing, so it won't affect our output at all.
Like anything, you can take this technique too far. If you find yourself embedding entire programs in a String
it's time to turn in your keyboard. However, if you find yourself slinging to_s()
calls like they are going out of style, there's probably some cleanup you could try.
Comments (7)
-
Peter Haza October 7th, 2008 Reply Link
I just love these posts! Ruby is still very new to me, but your posts just show how amazingly cool Ruby can be, and you show how you can take great advantage of this coolness.
Thank you!
-
I'm glad to hear readers are enjoying them. Thanks for the high praise.
-
-
Hey James
When my mother makes these great dishes that nobody else can do quite like her, I have taken to peering over her shoulder in the hopes I can learn to do it like her. Most of the time I'm busy guessing at ingredients and quantities, because she never weighs anything out. She does that by 'feel'. And most importantly: I watch her at work.
I'm guessing your posts and screencasts are made of:
- at least 5 cups of playful enthusiasm for Ruby
- heaps of programming skills
- 2 handfuls of intuition about pedagogy/teaching
You have a certain tone that says to me: 'I am having fun doing this'.
That makes it a joy to read, watch and learn.Thank you.
P.S. Despite my hard work, I'm pretty sure you can still smash windows with my homemade meatballs ;-)
-
I am definitely missing the "heaps of programming skills" you credit me with, but you're right on that I love what I do. Can you believe people pay me to play with Ruby (my favorite toy)? It still surprises me.
-
I'm nothing if not an amateur, so I might not be the best judge of what 'heaps of programming skills' contain :-)
They're no replacement for loving one's work, though.
For my part, I couldn't tell you precisely what keeps me going. It's a wide variety of factors. One such is definitely the kick I get when the core of a work in progress works for the first time. In those moments I always feel like laughing loudly and exclaiming 'It's alive! muhahahahaaaa!' :)
It's really insane the number of hours it can take me to get from no code to that moment.
Sometimes I wish I was a street sweeper. At least they have shorter work cycles.
-
-
Guilty as charged. I hereby repent and won't use
to_s
in that context anymore.But how about this one:
[first, last].join(' ')
I think that looks nicer, especially when there are many elements. You can still call
strip
on the result.-
That's a good point. It's worth noting that
join()
also includes ato_s()
call for all members:>> [42, nil, Object.new].join(" ") => "42 #<Object:0x5303a4>"
-